2007年11月7日星期三

[转载]Hibernate 继承中无法继承关联关系问题的解决方式

找了好久啊,总算找到了一篇解决Hibernate 继承“想当然问题”的论述,谢谢这位老兄细致的总结分析,我是怕丢失了这篇宝贵的文章,所以特地转载在这里了,也没有获得老兄的认可,希望能见谅了,呵呵。

from:http://clarkupdike.blogspot.com/2007/01/hibernate-mappedby-to-superclass.html

Hibernate alternatives for mappedBy to a superclass property

I've been working on a JPA/Hibernate prototype of an application that was previously mapped using Toplink. So this is the "meet in the middle" where there is an existing domain model that must be mapped to an existing schema. In reality, the domain model is somewhat free to change as long as the public interface stays the same. And if push comes to shove, schema changes are possible (but undesirable).


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 List externalContactAssignments;
...

}




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 List externalContactAssignments;

...

}



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 List externalContactAssignments;

}



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


@Entity
@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 List externalContactAssignments;

...
}


Anyway, so I'll be moving ahead with this last approach as it is closest to what I want.