简体   繁体   中英

Hibernate: how to reattach an externalized entity that has an @ElementCollection

Context

Stackoverflow has been invaluable to me over the years, and I wanted to give back by posting the question and the answer for something I spent a significant amount of time on recently.

Background

In my situation, I am storing the serialized JSON of our entities in Memcached. For various reasons, I didn't like the way hibernate's caching (2nd level and query cache) worked, so I decided to write my own caching layer. But this created a problem: there appeared to be no easy way to reattach a previously cached POJO back into a hibernate session, especially if there was an @ElementCollection involved.

Here's an approximate definition of some POJOs I wanted to deserialize from JSON, and reattach to a hibernate session:

@Entity
@Table
public class User extends AbstractModel {

    @ElementCollection
    @CollectionTable (name = "UserAttribute", joinColumns = { @JoinColumn (name = "UserId") })
    @MapKeyColumn (name = "Name")
    @Column (name = "Value")
    private Map<String, String> attributes = new HashMap<String, String>();
    ...
}

@Entity
@Table
public class Content extends AbstractModel {

    @ManyToOne(cascade = CascadeType.ALL) @JoinColumn (name = "UserId")
    private User user;

    @ElementCollection
    @CollectionTable(name = "ContentAttribute", joinColumns = { @JoinColumn(name = "ContentId") })
    @MapKeyColumn(name = "Name")
    @Column(name = "Value")
    private Map<String, String> attributes = new HashMap<String, String>();
    ...
}

In my searching for an answer to this question, the closest matches where these:

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM