Saturday, October 24, 2009

Polymorphic one to many relationships in Hibernate

These are some notes on mapping a polymorphic one-to-many relationship in Hibernate. I want to give credit to Clark Updike's blog for providing most of the answers. These notes reflect my own particular bias and needs. I wrote the code samples below in Groovy for ease of prototyping (they actually work). They should be close enough to Java to serve as concise pseudo code, even if you don't know Groovy. They aren't my real production entity classes, of course, but these simple classes are sufficient to illustrate the issues I grappled with.



The problem

A one-to-many relationship between Hibernate entities is commonly mapped as a bidirectional relationship, with a mappedBy attribute to indicate the inverse side of the relationship. The problem becomes more complicated when the back-reference property belongs in a superclass. Say we model a pet Owner who keeps multiple Pets. A Cat is a subclass of Pet, so its back reference to Owner would be a property of its superclass. The obvious way to model this would be:

@Entity
@Inheritance(strategy=InheritanceType.JOINED)
class Pet {
    @Id @GeneratedValue
    long id
    @ManyToOne
    Owner owner
}

@Entity
public class Cat extends Pet {}

@Entity
public class Dog extends Pet {}

@Entity
public class Owner {
    @Id @GeneratedValue
    long id
    
    @OneToMany(mappedBy='owner') // OK
    List<Pet> pets

    @OneToMany(mappedBy='owner') // Error
    List<Dog> dogs

    @OneToMany(mappedBy='owner') // Error
    List<Cat> cats
}


Unfortunately, Hibernate will throw exceptions for the dogs and cats collections:

org.hibernate.AnnotationException: mappedBy reference an unknown target entity property: Cat.owner in Owner.cats

The owner property belongs to the Pet superclass and cannot be used by the mappedBy attribute for a Cat. This seems counter-intuitive from the OOP perspective. A Hibernate developer explained the reasoning from the relational DB perspective why this happens with the table per subclass inheritance mapping. The reason is if you specify a superclass' property as the mappedBy value, you are in effect saying that a Pet is associated with the Owner (the foreign key is on Pet). This contradicts the one-to-many side of the mapping, which says that the Owner is associated with a Cat.

Unfortunately, the table per subclass mapping was what I wanted. While the samples here do not reflect it, I saw a need for polymorphic queries on the hierarchy as well as a lot of subclass-specific properties. The table per concrete class technique hinders polymorphism, and the table per class hierarchy (single table) technique prevents not null constraints on properties in subclass. I started with Clark's blog post and experimented with the options.

Using @MappedSuperclass

This is the only multi-table solution in Clark's blog post. This method replaces the @Entity annotation with @MappedSuperclass. The code would look like the following:


@MappedSuperclass
class Pet { ... }

@Entity
public class Cat extends Pet {}

@Entity
public class Dog extends Pet {}

@Entity
public class Owner {
    @Id @GeneratedValue
    long id
    
    @OneToMany(mappedBy='owner') // Error
    List<Pet> pets

    @OneToMany(mappedBy='owner') // OK
    List<Dog> dogs

    @OneToMany(mappedBy='owner') // OK
    List<Cat> cats
}

Unfortunately, this prevents polymorphism, because this is the table per concrete class mapping style. The pets collection caused the error:

org.hibernate.AnnotationException: Use of @OneToMany or @ManyToMany
targeting an unmapped class: Owner.pets[Pet]


This does not prevent polymorphic queries like searching for Pets: Hibernate will simply issue a separate select statement for each subclass. But it is inconvenient being unable map a collection of pets, even for use in queries.

Single table, without mappedBy

This was Clark's preferred solution, and understandably so because of its simplicity. By discarding mappedBy, you gave up the bidirectional association in favor of managing it exclusively on the Pet side. This is why we have insertable and updatable set to false. This does need the additional @Where clause on the subclass collections.


@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
class Pet { ... }

@Entity
public class Cat extends Pet {}

@Entity
public class Dog extends Pet {}

@Entity
public class Owner {
    @Id @GeneratedValue
    long id
    
    @OneToMany
    @JoinColumn(name="owner_id", insertable=false, updatable=false)
    List<Pet> pets

    @OneToMany
    @JoinColumn(name="owner_id", insertable=false, updatable=false)
    @Where(clause="dtype='Dog'")
    List<Dog> dogs

    @OneToMany
    @JoinColumn(name="owner_id", insertable=false, updatable=false)
    @Where(clause="dtype='Cat'")
    List<Cat> cats
}


Table per subclass, without mappedBy

The single-table code above worked fine, but I did want to use the table per subclass mapping. Can the same @JoinColumn annotation work with InheritanceType.JOIN? Alas, not quite. By specifying a join column for Pet and its subclasses, I caused Hibernate to generate join columns on every table. But because of the inheritance relationship, only the Pet join column was populated. That is, Pet.owner_id was populated for all subclasses of Pet, but Dog.owner_id and Cat.owner_id was always null. However, the SQL for the dogs collection looked (conceptually) like:

select * from Dog
inner join Pet on Dog.id = Pet.id
left outer join Owner on Pet.owner_id=Owner.id
where Dog.owner_id=?


Since Dog.owner_id was always null, the where clause would always return nothing for the dogs collection. To make this work, I had to use only collections of type Pet and add a discriminator property (no longer auto-generated, since this is not the single-table mapping):

@Entity
@Inheritance(strategy=InheritanceType.JOINED)
class Pet {
    @Id @GeneratedValue
    long id
    @ManyToOne
    Owner owner

    String dtype = this.class.simpleName
}

@Entity
public class Cat extends Pet {}

@Entity
public class Dog extends Pet {}

@Entity
public class Owner {
    @Id @GeneratedValue
    long id
    
    @OneToMany
    @JoinColumn(name="owner_id", insertable=false, updatable=false)
    List<Pet> pets

    @OneToMany
    @JoinColumn(name="owner_id", insertable=false, updatable=false)
    @Where(clause="dtype='Dog'")
    List<Pet> dogs

    @OneToMany
    @JoinColumn(name="owner_id", insertable=false, updatable=false)
    @Where(clause="dtype='Cat'")
    List<Pet> cats
}



So with some compromises, it looks like the table per subclass mapping could work out after all. I'm not so sure, though. The query for cats looks conceptually like:

select * from Pet
left outer join Dog on Pet.id=Dog.id
left outer join Cat on Pet.id=Cat.id
where Pet.dtype='Cat' and Pet.owner_id = ?


Yuck. It outer-joins all classes in the hierarchy. It's not clear from the simplified query above, but it retrieves the union of all properties in the entire hierarchy into the result set. For a real-world schema with lots of properties and more subclasses, that could be expensive. Plus, I would now have to cast the members of the dogs and cats collections.

Reconsidering

At this point, I reconsidered whether the conceptual elegance of the table per subclass mapping is worth it. I could not map collections typed to the subclasses I want. The resulting SQL queries are the sort that make ORM look ridiculous. On the other hand, I still wanted to be able to use not null constraints for some of my more substantial subclasses. As the Java Persistence with Hibernate book said, delegation is an option. If I can move most of the properties out of the subclasses to other entities, I can apply the not null constraints there.

Besides design purity, other factors I considered were the complexity and number of subclasses and the performance needs of this part of the schema. I won't say what I ultimately went with, since the decision isn't meaningful to you without the business context, but hopefully the options I described above are useful to you.

12 comments:

  1. => Is it possible to have the following mapping ? What should I change ?
    because if I try to query it :
    Criteria crit = super.getSession().createCriteria(TA.class);
    crit.createAlias("TE", "trk");
    crit.add(Restrictions.like("trk.id", "%"));

    I get :
    select ...............
    from TA this_
    inner join TE1 trk1_ on this_.TE_ENTITY_FK=trk1_.TE_KEY
    inner join TE2 trk1_ on this_.TE_ENTITY_FK=trk1_.TE_KEY
    where trk1_.TE_KEY like ?

    ORA-00918: column ambiguously defined.

    Thank you ORM !


    @Entity
    @Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
    public abstract class TE {

    @Id
    @GenericGenerator(name = "idGenerator", strategy = "assigned")
    @GeneratedValue(generator = "idGenerator")
    @Column(name="TE_KEY")
    private String id;

    @OneToMany
    @JoinColumn(name="TA_KEY", referencedColumnName="TE_KEY", insertable=false, updatable=false)
    private List< TA > taList;
    }

    @Entity
    public class TE1 extends TE {
    }

    @Entity
    public class TE2 extends TE {
    }


    @Entity
    @Inheritance(strategy=InheritanceType.SINGLE_TABLE)
    @DiscriminatorColumn(name="ta_type")
    public abstract class TA {

    @Id
    @GenericGenerator(name = "idGenerator", strategy = "assigned")
    @GeneratedValue(generator = "idGenerator")
    @Column(name="TA_KEY")
    private String id;

    public abstract void setTE(TE TE);

    public abstract TE getTE();
    }

    @Entity
    @DiscriminatorValue("TE1")
    public class TA1 extends TA {

    @ManyToOne(targetEntity=TE1.class)
    @JoinColumn(name="TE_ENTITY_FK")
    private TE TE;

    public TE getTE() {
    return TE;
    }

    public void setTE(TE TE) {
    this.TE = TE;
    }
    }

    @Entity
    @DiscriminatorValue("TE2")
    public class TA2 extends TA {

    @ManyToOne(targetEntity=TE2.class)
    @JoinColumn(name="TE_ENTITY_FK")
    private TE TE;

    public TE getTE() {
    return TE;
    }

    public void setTE(TE TE) {
    this.TE = TE;
    }

    }

    ReplyDelete
  2. Hello, many thanks for this post.

    Here is another solution:
    Pet: single table inheritance with all properties but no manyToOne properties.

    PetOwner abstract @MappedSuperClass inherit Pet and contains the owner manyToOne (and so owner can mapped by this property due to mappedsuperclass)

    Cat and Dog inherit PetOwner with a discriminatorValue

    This solution allows polymorphism, single table inheritance, discriminator value and mapped by property

    ReplyDelete
  3. => Cat and Dog inherit Pet. CatOwner and DogOwner inherit Owner. A Cat has a CatOwner. A Dog has a DogOwner. When fetching all Cat and Dog (All Pets), I want to inner join fetch CatOwner for Cat and DogOwner for Dog.

    => select p from Pet ...
    must return query like this :
    select ...............
    from Pet this_
    inner join CatOwner catowner1_ on this_.OWNER_FK=catowner1_.KEY
    inner join DogOwner dogowner1_ on this_.OWNER_FK=dogowner_.KEY

    But with your mapping I have something like this :
    select ...............
    from Pet this_
    inner join (select ... DogOwner union CatOwner) owner_ on owner_.KEY = this_.OWNER_FK
    => It has very bad performance ! For each Pet record, Hibernate generates an SQL UNION between all Owner subclasses to select the right Owner. Suppose Owner is a big table per class inheritance tree...(with 100 000 rows in each table)

    ReplyDelete
  4. Very good post. I am also struggling with the same thing as you. I wanted to avoid Nullable columns, but got huge headache with InheritanceType.JOINED after all.

    I think Hibernate is limiting advantages of Object Oriented programming quite much. There are quite nice things implemented in Hibernate but I just can not find a good use for those. As a result I have much less pain with dummy tables rather than with Hibernate inheritance.

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. Back to original problem, to avoid the errors with InheritanceType.JOINED instead of:

    @OneToMany(mappedBy='owner') // Error
    List< Dog > dogs

    @OneToMany(mappedBy='owner') // Error
    List< Cat > cats

    I tried to use:

    @OneToMany
    @JoinTable(name = "Pet", joinColumns = { @JoinColumn(name = "owner_id") }, inverseJoinColumns = { @JoinColumn(name = "id") }) // OK
    List< Dog > dogs

    @OneToMany
    @JoinTable(name = "Pet", joinColumns = { @JoinColumn(name = "owner_id") }, inverseJoinColumns = { @JoinColumn(name = "id") }) // OK
    List< Cat > cats

    And it worked well!

    ReplyDelete
  7. Know this is a bit old, but has anyone come up with a reason why this is not allowed in hibernate? I'm just looking for a valid explanation as to why this shouldn't work as people expect it to.

    There is an open is here: http://opensource.atlassian.com/projects/hibernate/browse/HHH-4233

    looking for some answers, but just thought I would see if anyone else has the answer.

    ReplyDelete
  8. There's a much simpler solution. Just do this:

    @Entity
    @Inheritance(strategy=InheritanceType.JOINED)
    class Pet {
    @Id @GeneratedValue
    long id
    @ManyToOne
    Owner owner
    }

    @Entity
    public class Cat extends Pet {}

    @Entity
    public class Dog extends Pet {}

    @Entity
    public class Owner {
    @Id @GeneratedValue
    long id

    @OneToMany(mappedBy='owner', targetEntity = Pet.class)
    List pets

    @OneToMany(mappedBy='owner', targetEntity = Pet.class)
    List dogs

    @OneToMany(mappedBy='owner', targetEntity = Pet.class)
    List cats
    }

    and everything will work as one would expect :)

    ReplyDelete
  9. @OneToMany(mappedBy='owner', targetEntity = Pet.class)
    List pets

    @OneToMany(mappedBy='owner', targetEntity = Pet.class)
    List dogs

    @OneToMany(mappedBy='owner', targetEntity = Pet.class)
    List cats

    Will not work: all 3 methods will return all Pets instead of Pets, Dogs or Cats...

    I would like to see a solution with MappedSuperclass. The only soluton I can imagine is to implement it by using the get/setDog/Cat and method annotation:

    @Entity
    public class Owner {
    ... //but with mehtod annotation instead of attribute

    List getPets() {
    return new ArrayList(dogs).addAll(cats);
    }
    }

    ReplyDelete
  10. This really helped me. Thanks for posting it.

    ReplyDelete
  11. excellent post - thank you.

    I have just had the same issue.
    For me the solution was to remove @Entity from Pet and mark it up as @MappedSuperclass as AT is asking.

    ReplyDelete
    Replies
    1. Yes, exacltly the same for me!
      Thank you very much!

      Delete