Tuesday, October 28, 2008

Delegation vs. Inheritance

The question "how do I change the type of my object" in JPA comes up now and then because developers know that an object's class is typically derived from an discriminator column on the database. They reason that if they can change the column value then class of the object can be changed. But it's not that simple.

What you're really trying to do is to change the class of an Entity object. Obviously this isn't something supported by Java nor by JPA. In EclipseLink it may be possible to hammer a discriminator column, invalidate the affected object in the cache, and then reread it. That could work but with a large number of caveats including ensuring you hold no references to the transformed object in your persistence context.

Instead, I'd suggest that if your domain model includes the ability to change the class of an Entity you refactor your model to use a delegation/role modeling approach rather than using inheritance. This allows you to change roles at runtime and even to have multiple concurrent roles (e.g., person could have a fireman role as well as a spouse role) and would behave accordingly.

Here's a simple example:

public enum PetType {
    CAT   { String speak() { return "meow"; } },
    DOG { String speak() { return "woof"; } };
    abstract String speak();
  }


@Entity
public class Pet {
    @Id
    @GeneratedValue
    private int id;
    private String name;
    
    public Pet() {
    }
    
    public Pet(String name) {
        super();
        this.name = name;
    }
    @Enumerated
    private PetType type = PetType.CAT;

        ...
    public String speak() {
        return this.getType().speak();
    }
}


        em.getTransaction().begin();
        Pet pet = new Pet("fluffy");
        System.out.println(pet.getName() + " is a " + pet.getType().toString() + " and says " + pet.speak());
        em.persist(pet);
        em.getTransaction().commit();
        
        em.getTransaction().begin();
        pet = em.find(Pet.class, pet.getId());
        pet.setType(PetType.DOG);
        System.out.println(pet.getName() + " is now a " + pet.getType().toString() + " and says " + pet.speak());
        em.getTransaction().commit();




fluffy is a CAT and says meow
[EL Fine]: Connection(27582163)--UPDATE SEQUENCE SET SEQ_COUNT = SEQ_COUNT + ? WHERE SEQ_NAME = ?
bind => [50, SEQ_GEN]
[EL Fine]: Connection(27582163)--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ?
bind => [SEQ_GEN]
[EL Fine]: Connection(27582163)--INSERT INTO PET (ID, NAME, TYPE) VALUES (?, ?, ?)
bind => [101, fluffy, 0]
fluffy is now a DOG and says woof
[EL Fine]: Connection(27582163)--UPDATE PET SET TYPE = ? WHERE (ID = ?)
bind => [1, 101]

One more thing

You could also implement methods on the PetType that took the Pet object and called back to the appropriate Pet method depending on the PetType. All in all this is a pretty flexible solution.

2 comments:

Rafael Chaves said...

Well, that works if your subclasses do not define additional state. Unless you are happy with the single-table-per-hierarchy ORM approach.

Shaun Smith said...

Yes you do need the same structure or you need to factor variable structure into the role object and move away from using an Enum like I did for this very simple example. Your delegate could be an Entity in a hierarchy itself.

The choice between delegation and inheritance for implementing variable behavior and storage is a very old OO technique and it fits well with the requirement to "change class" at runtime. And it is generally straight forward to implement in ORM.