Skip to content
Print

Polymorphic Relational Types in ActiveObjects

11
Dec
2007

Remember when I said that ActiveObjects would always attempt to take the simplest approach to any problem, even when it meant eschewing some more esoteric features?  Well, this is probably a bit of an exception.  Polymorphic types are most certainly not simple (at least, not from an ORM design standpoint) and I had to do a bit of hackery under the surface to make them work.  However, given the usefulness of this feature, I think that it was probably worthwhile.

Simply put, a polymorphic relational type is a table which has a relation with one or more other tables based on the value of a non-constrained field.  Got that?

Maybe a diagram would be helpful…

image

We’ve all seen this pattern at some time or other.  Rather than having insurancePolicies contain n mapping fields (e.g. “employeeID” and “managerID”), we make the mapping polymorphic and provide an ancillary type field which specifies which table is actually mapped.  This simplifies queries, not to mention making things far more extensible.  For example, if we wanted to add a janitors table here, we wouldn’t need to change insurancePolicies at all to allow mapping.  Rather, we just add the table and define (in our documentation) another type value which specifies janitors as opposed to employees as the mapped table.

So the concept itself is fairly straightforward.  The difficulties come when you try to map this into ORM-land.  At first glance, it seems like it should be a cakewalk.  Right away we can see some inline inheritance shared between employees and managers, so maybe our entities will look like this:

public interface Person extends Entity {
    public String getFirstName();
    public void setFirstName(String firstName);
 
    public String getLastName();
    public void setLastName(String lastName);
 
    @OneToMany
    public InsurancePolicy[] getPolicies();
}
 
public interface Employee extends Person {
    public int getHourlyWage();
    public void setHourlyWage(int wage);
}
 
public interface Manager extends Person {
    public long getSalary();
    public void setSalary(long salary);
}
 
public interface InsurancePolicy extends Entity {
    public int getValue();
    public void setValue(int value);
 
    public Person getPerson();
    public void setPerson(Person person);
}

As with any other form of table inheritance in ActiveObjects, the supertype doesn’t correspond to a table.  There’s no multi-JOIN mapping going on.  The difference is that now we’re not only inheriting fields from the supertype, but the inheritance also allows other entities to treat the type polymorphically on the supertype.  We can assume that the personType field is auto-generated by the ORM during the migration.  Seems reasonable enough.

Unfortunately, as it stands right now, ActiveObjects will blissfully recurse into the InsurancePolicy entity, see the getPerson method and precede to generate a table for Person, rather than ignoring Person in favor of its subtypes.  This is because AO has no way of knowing that Person even has subtypes.  Java doesn’t provide a convenient way of getting derived interfaces or anything so nice.  So as far as the migration process is concerned, Person is a totally valid entity which requires a peered table.

The solution here is fairly simple, just tack on an annotation to the Person type to indicate to the schema generator that any relations on the type are to be polymorphic.  I vacillated for a while between @Abstract and @Polymorphic, eventually choosing the latter.  However, if you have any strong preferences either way, let me know!

The new Person declaration looks something like this:

@Polymorphic
public interface Person extends Entity {
    // ...
}

Ok, one problem down.  Now we run into the issue of the type mapping value itself (e.g. “employee”, “manager”, etc).  This seems like it should be something AO could handle for us auto-magically, right?  After all, there’s already a hierarchy in place for generating table names from a given entity type, extending this to handle polymorphic mapping values should be trivial.

The problem is that the process of converting an entity type into a table name is non-invertible, meaning that you can’t just feed in a table name and get a valid entity type out the other end.  Information is actually lost in the transition between type and table.  Think about it; the process starts with a fully-qualified class name, strips off the package info, messes with case, special chars and (potentially) plurality.  By the time we get to the result, the table name is so mangled and transformed as to bear absolutely no resemblance to the original type (at least from the perspective of a generic algorithm).

So the current table name generation hierarchy is insufficient for our purposes.  Potentially it could generate the values, but it certainly couldn’t retrieve the type which corresponds with those values.  To solve this problem, we need to introduce a whole new generator to the group: PolymorphicTypeMapper.

All that our type mapper implementation needs to do is define a process by which types are transformed into string values and back again.  We could just rely on storing the fully-qualified class name, but this is both rigid (hard to refactor) and ugly.  No, this is one place where I think we can do something a bit more sophisticated.

It is possible to simply force the users to specify mappings from type to String in the form of a Map<Class, String>, and in the end, this is what our process will boil down to.  However, I think we can add some syntactic sugar to the process which will allow it to default to a de-pluralized (if necessary) version of the table name:

EntityManager em = new EntityManager(...);
em.setPolymorphicTypeMapper(new DefaultPolymorphicTypeMapper(
     Employee.class, Manager.class));

This way, DefaultPolymorphicTypeMapper will auto-generate the mappings based on the classes we pass.  Since we’re statically specifying which subtypes will be used polymorphically, we’re still giving the system enough information to produce an invertible process.  We are coupling our EntityManager initialization a bit to our entity hierarchy.  However, if you’re using migrations, chances are you’ve already taken this plunge.  Anyway, I think it’s about as clean as the syntax can possibly become (with the possible exception of a less verbose class name).

Thanks to the introduction of the type mapper, we can now use our polymorphic hierarchy in the following way:

// ...
Employee employee = em.get(Employee.class, 1);
InsurancePolicy[] policies = employee.getPolicies();
 
for (InsurancePolicy policy : em.find(InsurancePolicy.class)) {
    System.out.println("Found policy with value: " + policy.getValue());
 
    if (policy.getPerson() instanceof Employee) {
        System.out.println("Belongs to an employee");
    } else if (policy.getPerson() instanceof Manager) {
        System.out.println("Belongs to a manager");
    }
}

Okay, maybe a bad example, but the functionality is expressed.  This sort of mapping can be a very powerful tool for reducing code hassle and improving extensibility.  It even works with many-to-many and (the new) one-to-one relations.  With this functionality, ActiveObjects table inheritance is more or less feature-complete.  Unless I’m missing something obvious, this provides about all the reasonable functionality you could possible want from an entity inheritance scheme.

Currently, this feature is just in the SVN trunk/ and slated for release in the upcoming 0.7 build.  There are still a few bugs to be worked out (specifically in the cache expiry mechanism for complex polymorphic many-to-many relations), but everything should be more or less stable and usable.  Enjoy!

Post a Comment

Comments are automatically formatted. Markup are either stripped or will cause large blocks of text to be eaten, depending on the phase of the moon. Code snippets should be wrapped in <pre>...</pre> tags. Indentation within pre tags will be preserved, and most instances of "<" and ">" will work without a problem.

Please note that first-time commenters are moderated, so don't panic if your comment doesn't appear immediately.

*
*