If you have used Hibernate and mapped a one-to-many relationship you've probably come across the "delete orphan" feature. This feature offers cascade delete of objects orphaned by code like the following:
Preference pref = getSomePreference(); user.getPreferences().remove(pref);
In the above code, a specific Preference is removed from a User. With the delete orphan feature, and assuming there is an active transaction associated with a session, the preference that was removed from the user is automatically deleted from the database when the transaction commits. This feature is pretty handy, but can be tricky if you try to write clever code in you setter methods, e.g. something like this:
// Do not do this! public void setPreferences(SetnewPreferences) { this.preferences = newPreferences == null ? new HashSet<Preference>() : newPreferences; }
Code like the above results in a HibernateException with the following message if you pass null into setPreferences and try to save the user object:
A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance
What is happening here is that Hibernate requires complete ownership of the preferences collection in the User object. If you simply set it to a new object as in the above code sample, Hibernate is unable to track changes to that collection and thus has no idea how to apply the cascading persistence to your objects! The same error will occur if you passed in a different collection, e.g.:
user.setPreferences(new HashSet<Preference>());
So the point is that Hibernate's delete orphan abstraction is leaking into your domain model object. This is pretty much unavoidable but is a leaky abstraction nonetheless that developers need to be aware of lest they run into the error mentioned above.
So how can you avoid this problem? The only sure way that I know of is to make the setter method private, since passing any new collection or null results in the "owning entity" error. This way only Hibernate will use the setter method to load up user objects (it invokes the method reflectively after setting it accessible via the Reflection API). Then you could add a method addPreference to your code which is the public API for adding preferences. Anyone could of course use reflection to do the same thing Hibernate is doing, but then all bets are off as they are subverting your public API. For example:
public void addPreference(Preference p) {
getPreferences().add(p);
p.setUser(this);
}
This has the nice side effect of establishing the bi-directional relationship between user and preference, assuming your model allows bi-directional navigation. You could also add a null check if you are paranoid. Removing a preference from a user is equally simple. You can write a helper method removePreference or you could call the getter and then call remove as shown here:
user.getPreferences().remove(aPref);
Essentially, you can operate on the collection returned by getPreferences as a normal Java collection and Hibernate will do the right thing. Since you are operating on the collection maintained and observed by Hibernate, it is able to track your changes, whereas replacing the collection wholesale makes Hibernate really mad, since it believes it is the sole proprietor of the collection, not you! For example, if you want to remove all the user's preferences you could write the following:
user.getPreferences().clear();
Note that all the above discussion refers to one-to-many relationships that specify the delete orphan cascade type; usually you will specify both "all" and "delete-orphan." In cases where you are only using the "all" cascade option, the semantics are quite different. Assuming the normal case where the "many" side of the relationship owns the relationship -- i.e. you used inverse="true" in your mapping file or @OneToMany(mappedBy = "user") if using annotations - then you must explicitly delete the child objects as Hibernate will only track that side of the relationship. For example, if cascade is "all" and you remove a preference from a user and then save the user, nothing happens! You would need to explicitly delete the preference object, as shown here:
// Assume only using cascade="all" and an inverse="true" mapping in User.hbm.xml user.getPreferences().remove(aPref); // does not cause database delete session.delete(aPref); // this causes the database deletion
One last thing to note in the above is that you must remove aPref from the user, or else Hibernate will throw an exception stating that the preference would be re-saved upon cascade! So if the User object is in the Session, remember you need to undo both sides of the relationship for things to work properly.
2 Comments
Leave a comment
0 TrackBacks
Listed below are links to blogs that reference this entry: Let's Play "Who Owns That Collection?" With Hibernate.
TrackBack URL for this entry: http://www.nearinfinity.com/mt/mt-tb.cgi/474



Scott, you are being quoted for this solution across a wide audience, so let me thank you for this solution.
However, I have a different problem and I am exasperated
If it were simple parent.setChildren() or parent.getChildren(), that's one thing.
I have a static query, from which I am loading wrappers to the children..
List mylist = Query.list();
Query has been configured with criteria and order.
There is no way this query is going to return the original child object collection, so I cannot exactly play the game that you suggest for Hibernate to completely "own" the collection. I am lost at this point, any hints for me, please?!
Srinivas, I'm not sure I understand how the query is created and what it returns, but if you do a Hibernate HQL or Criteria query which returns a List of Foo objects, and the Foo objects each have a collection of Bar objects, then all of those objects (the Foos and the Bars) will be known to Hibernate, since it will have loaded up all those objects while returning the query results. So if, for each Foo object in the result list, you do something like List bars = foo.getBars(), then the bars collection will in fact be owned by Hibernate. You would only cause a problem if you tried to replace the Bar collection in one of the Foo objects, e.g. if you did something like: aFoo.setBars(new HashSet()).
The game I was talking about was really that any collection in a Hibernate-managed domain object must not be replaced with a new collection or set to null, else you will get the "A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance" error.
If this isn't the case you are talking about, or I have misunderstood, please reply again.