So I did find an answer, although it is a bit weird...
So first off, there is no problem moving a child from one parent to the other
except in the case where I delete the old parent. I came up with this solution:
Code:
Batch b = new Batch();
b.addChildren( new Document(), new Document() );
hibernate.save(b);
hibernate.flush(); //documents and batch are persisted
Batch b2 = new Batch();
for( Document d : b.getChildren() ) {
//d.setParent( null );
d.setParent( b2 );
b2.addChildren( d );
}
hibernate.save( b2 );
hibernate.flush();
hibernate.refresh( b ); //ideal solution
//b.setChildren( null ); //THIS WORKS TOO!
//b.getChildren().clear(); //this doesn't work; hibernate thinks the children should be deleted.
assertEquals( 0, b.getChildren().size() );
hibernate.delete( b );
hibernate.flush(); //ObjectDeletedException if children are still referenced by b's PersistentSet
b2 = (Batch)hibernate.get( Batch.class, 1L );
assertEquals( 2, b2.getChildren().size() );
Hibernate was trying to cascade the delete to the child documents even though they were moved to a new Batch (b2). This is because the children Set instance (PersistentSet) hangs on to references of child instances that were removed.
So a slightly hack-ish way to avoid this is to call b.setChildren(null) so the PersistentSet is no longer referenced, and Hibernate cannot find references to the child objects. This is not a great idea (setChildren() is package-protected anyway) but it seems to work.
If I do a refresh() on the old batch before deleting it, Hibernate realizes the child documents have been moved, and does not attempt to delete them. I think this is the correct operation.
What's odd, however, is that Hibernate doesn't recognize that the still-referenced children of "b" are stale and do not actually belong there.