I routinely have to merge deep object graphs originating from external systems (lacking my IDs), and it is almost guaranteed that any update will contain at least one of these transient-but-naturally-matches-a-persistent-collection-member merge landmines. I have created a piece of disgusting, hacky code to deal with this issue (below) by recursively digging through the graph to find and re-ID transient collection members that match the natural ID of an existing member. I follow this up with a merge() and update() and this seems to work OK.
This is just one of many issues that are starting to feel like punishment for having this use case. Life is great when every object you need to persist either originated from the DB or is new, but sadly I have to deal with matching detached entities by natural ID all over the place. I am surprised to see composite PKs posed as a solution since my experience trying to use them was that the Hibernate team's general attitude was that they weren't recommended and were mostly only supported for legacy systems.
I am obviously not the only one to feel stuck between these options:
https://hibernate.onjira.com/browse/HHH-2896. Unified support for ID and natural ID across the entire persistence API would be an amazingly powerful tool, but unfortunately I have seen no acknowledgement that this is even an issue much less an idea of if/when it will be implemented (see the crickets chirping over here:
https://forum.hibernate.org/viewtopic.php?f=1&t=1011626).
I have opted for surrogate PKs in the end. My thoughts on the painful experience:
Composite PKs are great for detached objects, but:
-Your model gets flooded with inherited composite IDs
-Trying to remedy this by mapping child FKs to a scalar UK in a composite ID parent buys you this bug (3.6):
https://forum.hibernate.org/viewtopic.php?f=1&t=1011670-You lose envers auditing support
-Based on the above 2, you can probably expect to find other holes in stated behavior of Hibernate when it comes to composite PKs
-Your DBA hates you
-Performance takes a hit from the sheer volume of repeated information
On the other hand if you use surrogate PKs when you have to deal with detached objects:
-You need to either hack something up like I did or manually pick though your entire graph to weed out the transients that match persistent collection members
-You lose session.get() and 2nd level caching by natural IDs. Only option left is query caching. Not very fun if, like me, you are receiving 10,000,000s of detached objects and needing to resolve them quickly against potential DB or session matches. I have resorted to managing my own caches of natural ID-ed objects in EHCache
-Defining natural UKs containing an FK parent ID gets awkward
Code:
@SuppressWarnings("rawtypes")
protected void reattachVolatileCollections(Object detached, Object persistent, Class<?> clazz, Set<Object> alreadyProcessed) throws Exception{
if (!alreadyProcessed.contains(detached)){
Session session = sessionFactory.getCurrentSession();
ClassMetadata md = sessionFactory.getClassMetadata(clazz);
if (md != null){
for (String prop : md.getPropertyNames()){
PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(detached, prop);
if (Collection.class.isAssignableFrom(pd.getPropertyType()) || Map.class.isAssignableFrom(pd.getPropertyType())){
Object dets = md.getPropertyValue(
detached,
prop,
session.getEntityMode());
Object dbs = md.getPropertyValue(
persistent,
prop,
session.getEntityMode());
Iterator it = null;
if (Collection.class.isAssignableFrom(pd.getPropertyType())){
it = ((Collection)dets).iterator();
}else{
it = ((Map)dets).values().iterator();
}
while (it.hasNext()){
Object det = it.next();
Iterator dbit = null;
if (Collection.class.isAssignableFrom(pd.getPropertyType())){
dbit = ((Collection)dbs).iterator();
}else{
dbit = ((Map)dbs).values().iterator();
}
while (dbit.hasNext()){
Object db = dbit.next();
if (db.equals(det)){
md.setIdentifier(
det,
md.getIdentifier(db, session.getEntityMode()),
session.getEntityMode());
reattachVolatileCollections(det,db,getTrueType(det),alreadyProcessed);
}
}
}
}
}
}
}
}