Picking up where the posters left off from the links in the question, here's the approximate/relevant code for how I managed to reattach. Below that is an outline of what's going on.
@Repository
public abstract class DaoHibernate<T> implements Dao<T> {
@Override
public T reattach(T entity) {
if (getCurrentSession().contains(entity)) {
return entity;
}
if (entity instanceof User) {
return (T) reattachedUser((User) entity);
}
if (entity instanceof Content) {
Content content = (Content) entity;
User user = content.getUser();
if (!currentSession().contains(user)) {
content.setUser(reattachedUser(user));
}
content.setAttributes(persistentAttributesMap(content.getId(), content.getAttributes(), Content.class);
getCurrentSession().lock(content, LockMode.NONE);
return entity;
}
throw new UnsupportedOperationException("reattach is not supported for entity: " + entity.getClass().getName());
}
private User reattachedUser(User user) {
user.setAttributes(persistentAttributesMap(user.getId(), user.getAttributes(), User.class));
getCurrentSession().lock(user, LockMode.NONE);
return user;
}
@SuppressWarnings ("unchecked")
private Map<String, String> persistentAttributesMap(long id, Map<String, String> attributes, Class clazz) {
SessionFactory sessionFactory = getSessionFactory();
Session currentSession = sessionFactory.getCurrentSession();
String role = clazz.getName() + ".attributes";
CollectionPersister collectionPersister = ((SessionFactoryImplementor) sessionFactory).getCollectionPersister(role);
MapType mapType = (MapType) collectionPersister.getCollectionType();
PersistentMap persistentMap = (PersistentMap) mapType.wrap((SessionImplementor) currentSession, attributes);
persistentMap.setOwner(id);
persistentMap.setSnapshot(id, role, ImmutableMap.copyOf(attributes));
persistentMap.setCurrentSession(null);
return persistentMap;
}
...
}
Walk through
As you can see, we have to ensure we never try to reattach an entity that is already in the current session, or else hibernate will throw an exception. That's why we have to do getCurrentSession().contains(entity)
in reattach()
. Care must be taken here using contains()
, because hibernate will not use entity.hachCode()
to lookup the entity, but rather System.identityHashCode(entity)
, which ensures not only that it is an equivalent instance, but the exact same instance that may already be in the session. In other words, you will have to manage reusing instances appropriately.
As long as associated entities are marked with Cascade.ALL
, hibernate should do the right thing. That is, unless you have a hibernate managed collection like our @ElementCollection
map of attributes. In this case, we have to manually create a PersistentCollection
(PersistentMap
, to be precise) and set the right properties on it, as in persistentAttributesMap
, or else hibernate will throw an exception. In short, on the PersistentMap
, we have to:
- Set the owner and snapshot key as the id of the owning entity
- Set the snapshot role as the fully qualified entity.property name, as hibernate sees it
- Set the snapshot
Serializable
argument as an immutable copy of the existing collection
- Set the session to
null
so hibernate won't think we're trying to attach it to the existing session twice
To complete the reattachment, call session.lock(entity, LockMode.NONE)
. At this point, as far as I can tell from my testing, hibernate respects this entity and persists all changes correctly when you call saveOrUpdate()
.
Caveats
I realize this is not a generic solution for all cases. This was just a quick solution to my specific problem that others can hopefully utilize and improve upon. Software is iterative.