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.
=> Is it possible to have the following mapping ? What should I change ?
ReplyDeletebecause 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;
}
}
Hello, many thanks for this post.
ReplyDeleteHere 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
=> 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.
ReplyDelete=> 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)
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.
ReplyDeleteI 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.
This comment has been removed by the author.
ReplyDeleteBack to original problem, to avoid the errors with InheritanceType.JOINED instead of:
ReplyDelete@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!
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.
ReplyDeleteThere 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.
There's a much simpler solution. Just do this:
ReplyDelete@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 :)
@OneToMany(mappedBy='owner', targetEntity = Pet.class)
ReplyDeleteList 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);
}
}
This really helped me. Thanks for posting it.
ReplyDeleteexcellent post - thank you.
ReplyDeleteI 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.
Yes, exacltly the same for me!
DeleteThank you very much!