While trying to map a relationship to a superclass, I assumed that O/R relationships can be inherited in a manner analogous to OO. So I naively assumed I could:
make the superclass an @Entity
use single table inheritance
use a @ManyToOne in the superclass (and then do a @OneToMany with a mappedBy=
in the other side of the relationship) set up a discriminator column on the superclass
provide discriminator values in the subclasses
and viola... other classes could then hold references to the sublclasses.
So the key assumption here is that mappedBy could simply reference the property of the subclass even though the property is actually in the superclass (as you can do in an OO sense). But I ran into problems trying this. Such as: javax.persistence.PersistenceException: org.hibernate.AnnotationException: mappedBy reference an unknown target entity property:. And no, it's not simply a field vs. accessor visibility issue. You can try different variations (including the sin of making it a public reference) and it will have no effect.
So this explores some of the options that were tried, and what the tradeoff's are...
In the examples that follow (the naive and wrong way):
ExternalContactAssignment is the subclass of ContactAssignment (via single table inheritence)
APLEntity is the class that has the @OneToMany to a subclass (ExternalContactAssignment)
ContactAssignment is the superclass that has the @ManyToOne back-reference to APLEntity
The code looked like this:
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="internal_ind", discriminatorType = DiscriminatorType.STRING)
@Table(name="CONTACT_ASSIGNMENT")
public abstract class ContactAssignment {
// bidirectional
@ManyToOne
@JoinColumn(name="APL_ENTITY_SEQ_NUM", nullable=false)
private APLEntity aplEntity;
...
}
@Entity
@DiscriminatorValue("N")
public class ExternalContactAssignment extends
ContactAssignment {
// nothing relevant in this class
...
}
@Entity
@org.hibernate.annotations.Entity
@Inheritance(strategy=InheritanceType.JOINED)
@Table(name="APL_ENTITY")
public abstract class APLEntity
implements ExternalContactAssignable {
@OneToMany(mappedBy="aplEntity")
@org.hibernate.annotations.Cascade({org.hibernate.annotations.CascadeType.ALL,
org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
@org.hibernate.annotations.Where(clause="internal_ind='N'")
private ListexternalContactAssignments;
...
}
Note: I'm stripping the code down to the bare minimum here (the real classes have lots of other stuff not relevant to the problem).
According to Emmanuel Bernard (Hibernate developer), it is semantically incorrect to assume this mapping structure to work as expected.
There are three approaches, which vary in
How much schema change you'll live with
Can you add new tables?
Can you add new columns?
Tradeoffs between relational integrity and OO-to-relational consistency
If you can live with superclass not being an @Entity
Here are the three approaches...
@MappedSuperclass with Column per Subclass
Approach:
ContactAssignment is mapped with an @MappedSuperclass instead of an @Entity.
ContactAssignment (superclass) maintains the references to APLEntity
Table per subclass model (new table(s) required). Discriminators cannot be used (they'll be ignored).
By using @MappedSuperclass, you lose the ability to have a relationship to the superclass. You also lose polymorphic queries when using straight JPA--although Hibernate queries will still be polymorphic.
I didn't pursue this as I'm trying to minimize schema change (an evaluation criteria for Hibernate). However, the mapping would look something like this:
@MappedSuperclass
public abstract class ContactAssignment {
...
@ManyToOne
@JoinColumn(name = "APL_ENTITY_SEQ_NUM", nullable = false)
public APLEntity getAplEntity() {
return aplEntity;
}
public void setAplEntity(APLEntity aplEntity) {
this.aplEntity = aplEntity;
}
}
@Entity
@Table(name="EXTERNAL_CONTACT_ASSIGNMENT") // class-specific
public class ExternalContactAssignment extends ContactAssignment {
// not much needed
}
@Entity
@org.hibernate.annotations.Entity
@Inheritance(strategy = InheritanceType.JOINED)
@Table(name = "APL_ENTITY")
public abstract class APLEntity {
@OneToMany(mappedBy="aplEntity")
@org.hibernate.annotations.Cascade({org.hibernate.annotations.CascadeType.ALL,
org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
@org.hibernate.annotations.Where(clause="internal_ind='N'")
private ListexternalContactAssignments;
...
}
Relationship Column per Subclass with Discriminator Column
Approach:
Single table model
ExternalContactAssignment (subclass) maintains the references to !APLEntity
Distinct relationship (FK) column used for each subclass
Still requires a discriminator column
Duplicative @Where and @DiscriminatorColumn's (bit of a wart).
This is Emmanuel Bernard's recommended approach, as being the most consistent between the object model and the relational model.
Consequences are that pure JPA queries are no longer polymorphic (but Hibernate queries still should be). You can no longer have a relationship to the superclass ContactAssignment. It feels less "OO" since you are forced to push the relationship down to the subclass(es)... I want OO considerations to drive this, not O/R mapping considerations (transparency!). On the relational side, things get ugly. Each subclass requires its own FK column out to the APL_ENTITY table. Although this is probably why Emmanuel says it's the most consistent, I don't think it's worth the price. XOR columns like that don't play well with referential integrity. A given row should only have one value populated no matter how many subclasses you have. It make it harder to query and index, and conceptually make the design harder to understand--and it only gets worse as you add more subclasses to the mix. It also doesn't make sense to me to have a discriminator column and still require multiple FK cols. Here it is:
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="internal_ind", discriminatorType = DiscriminatorType.STRING)
@Table(name="CAU_CONTACT_ASSIGNMENT") // Note: referencing a new modified table
public abstract class ContactAssignment {
public abstract void setAplEntity(APLEntity aplEntity);
public abstract APLEntity getAplEntity();
}
@Entity
@DiscriminatorValue("N")
public class ExternalContactAssignment extends
ContactAssignment {
// bidirectional
@ManyToOne
@JoinColumn(name="EXT_APL_ENTITY_SEQ_NUM")
private APLEntity aplEntity;
@Override
public APLEntity getAplEntity() {
return aplEntity;
}
@Override
public void setAplEntity(APLEntity aplEntity) {
this.aplEntity = aplEntity;
}
}
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
@Table(name="APL_ENTITY")
public abstract class APLEntity
implements ExternalContactAssignable {
@OneToMany(mappedBy="aplEntity", cascade={CascadeType.ALL})
@org.hibernate.annotations.Cascade(value=org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
@org.hibernate.annotations.Where(clause="internal_ind='N'")
private ListexternalContactAssignments;
}
Unidirectional Read Only Back-Reference
If I understand this correctly, this is basically ignoring Hibernate's ability to manage a bidirection relationship, and mapping a unidirectional read-only back reference from ContactAssignment to APLEntity. I believe this is what is discussed in Section 6.4.3 of Java Persistence With Hibernate. Making the back reference read-only tells Hibernate not to do a duplicative update when a ContactAssignment changes an APLEntity reference. Approach:
ContactAssignment is mapped with an @Entity.
ContactAssignment (superclass) maintains the references to !APLEntity
Single table model
Still requires a discriminator column
Duplicative @Where and @DiscriminatorColumn's (bit of a wart).
Emmanuel describes this approach as making the data design weaker. I'm not sure exactly how that is (or maybe what it means), or what tradeoff's are implied, but it's certainly is closest to what I was looking for:
no schema change required --> so no loss of relational integrity possible
scales easily with additional subclasses
let's OO considerations drive domain model design
Here's what it looks like
@EntityAnyway, so I'll be moving ahead with this last approach as it is closest to what I want.
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="internal_ind", discriminatorType = DiscriminatorType.STRING)
@Table(name="CONTACT_ASSIGNMENT")
public abstract class ContactAssignment {
@ManyToOne
@JoinColumn(name="APL_ENTITY_SEQ_NUM", nullable=false)
private APLEntity aplEntity;
...
}
@Entity
@DiscriminatorValue("N")
public class ExternalContactAssignment extends
ContactAssignment {
// not much needed
}
@Entity
@org.hibernate.annotations.Entity
@Inheritance(strategy=InheritanceType.JOINED)
@Table(name="APL_ENTITY")
public abstract class APLEntity
implements ExternalContactAssignable, Authorizable {
@OneToMany(cascade={CascadeType.ALL})
@JoinColumn(name="APL_ENTITY_SEQ_NUM", insertable=false, updatable=false)
@org.hibernate.annotations.Cascade(value=org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
@org.hibernate.annotations.Where(clause="internal_ind='N'")
private ListexternalContactAssignments;
...
}