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.