To fill in some detail about the underlying hibernate behavior.
Parent parent = Parent.findById(1L); //loads the object. Relationship properties are PersistentBag's under the hood.
parent.children.add(new Child()); //This calls PersistentBag.add(). Child() will be queued to be added, not actually added
parent.children.size(); //this or any other read operation, triggers AbstractPersistentCollection.initialize().
parent.save(); // save calls getOrphans() on the collection, which cannot handle Transient entries in the snapshot. An exception is thrown.
Here's what PersistentBag::add() looks like:
/**
* @see java.util.Collection#add(Object)
*/
public boolean add(Object object) {
if ( !isOperationQueueEnabled() ) {
write();
return bag.add(object);
}
else {
queueOperation( new SimpleAdd(object) );
return true;
}
}
For some reason the code goes down the else fork and queues the add without initializing. (write() would trigger initialization). If this is the correct behavior, then flushing the queue before taking the CollectionEntry snapshot seems wrong. Alternately if flushing before snapshoting is right, it seems like allowing these transient entries to be queued is wrong.
Note: If you're trying to replicate the issue in an IDE, be careful about watches and whanot. I found that in InteilliJ IDEA, the default watches would actually trigger initialization. (For example if I added a break point in PresistentBag, the automatic watches would trigger read()'s)
-John
On Friday, September 21, 2012 9:24:52 AM UTC-7, Julian wrote:
It's semi-relevant, but not quite the same thing. The linked example deals with a set of 3 model objects.
Item has 1=>N Positions with CascadeType.ALL
Position has 1=>N Orders with CascadeType.ALL
The example has the following steps:
- Load an item, whose positions collections is lazy-loaded and not yet initialized.
- Load a single order whose position's item is the original item. This is done without initializing the item instance's positions collection
- Starting from the order, navigate to the loaded position and update a value
- Save the item. Because the positions collection is still not initialized (it hasn't been touched and thus lazy-loading has not been done on it) the save will not cascade to the position, even though it is part of the relationship.
The end result of this is essentially unexpected behavior (changes are not persisted to the Position), but not an exception. Furthermore, this example applies to play version 1.2.4 whereas the exception in my example only occurs in 1.2.5.
The problem does seem to have a bit to do with collection initialization though. I have since discovered that if the collection is fully initialized BEFORE the child is added to it, the exception does not occur. (One way of doing this being to call .size() on the collection as in the example).
The weird thing is that I would expect adding an object to the collection to initialize it. See here for a discussion on that behavior (the discussion seems to apply to the version of hibernate in use by play 1.2.4 and 1.2.5):
A colleague found out that hibernate's persistent bag implementation contains a snapshot of the bag's contents that is set when the collection is initialized. The purpose of the snapshot is to detect orphans, e.g. whether the bag contained an item when it was initialized and no longer does when it is being persisted. The problem seems to be that the calling .add() doesn't initialize the bag, so when it is initialized later, the snapshot contains the transient object, which causes an exception during the orphan detection process. We haven't figured out of this is expected behavior, a bug of hibernate, or a bug of play itself (or its patched version of hibernate).