Search This Blog

Saturday 31 March 2012

Merging A Detached Object

In an earlier post we saw how to reattach detached objects using update() and lock(). Consider the below code where we try to reattach an object.
public static  void needForMerge() {
    Session session = sessionFactory.openSession();
    System.out.println("Fetching the object via get call");
    Entity entity1 = (Entity) session.get(Entity.class, 1);        
    String data = entity1.getData();
    session.close();//entity1 is detached
    System.out.println("data is " + data);
    entity1.setData("New data!!!");//detached object updated. hibernate not aware of change
    System.out.println("Opening a new session");

    Session newSession = sessionFactory.openSession();
    Transaction transaction = newSession.beginTransaction();
    //loading the previous object from database
    Entity entity2 = (Entity) newSession.get(Entity.class, 1);        
    System.out.println("The data now loaded from db is " + entity2.getData());
    //trying to reattach the first object
    newSession.update(entity1);
    transaction.commit();
    newSession.close();
}
We would expect the code to make entity1 persistent again. However it throws an exception.
exception in thread "main" org.hibernate.NonUniqueObjectException: a different o
bject with the same identifier value was already associated with the session: [c
om.model.Entity#1]
    at org.hibernate.engine.StatefulPersistenceContext.checkUniqueness(StatefulPers
istenceContext.java:590)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.performUpdate(Defau
ltSaveOrUpdateEventListener.java:284)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsDetached(De
faultSaveOrUpdateEventListener.java:223)
    at org.hibernate.event.def.DefaultUpdateEventListener.performSaveOrUpdate(Defau
ltUpdateEventListener.java:33)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(Defa
ultSaveOrUpdateEventListener.java:70)
The code fails because of the presence of an entity with same identifier in the persistence context. At any given time we can have only instance of a record associated with a session. In this entity2 is there in the session and we try to add entity1. As both are referring to the same record there is a failure.
The solution to above issue would be to
  1. Reattach entity1 to the second session
  2. Now try to load entity2
  3. As entity1 is already present in the persistence cache, Hibernate will simply return a reference to entity1
The other option would be to let Hibernate merge the two objects, without us worrying about it.
public static  void mergeCase1() {
    Session session = sessionFactory.openSession();
    System.out.println("Fetching the object via get call");
    Entity entity1 = (Entity) session.get(Entity.class, 1);        
    String data = entity1.getData();
    session.close();
    System.out.println("data is " + data);
    entity1.setData("mergeCase1");
    Session newSession = sessionFactory.openSession();
    Transaction transaction = newSession.beginTransaction();        
    Entity entity2 = (Entity) newSession.get(Entity.class, 1);
    System.out.println("The data now loaded from db is " + entity2.getData());
    System.out.println("(entity1 == entity2) ? " + (entity1 == entity2));
    Entity mergedEntity = (Entity) newSession.merge(entity1);  
//it searches in pc, finds entity2 merges it with entity1 and returns entity2
    System.out.println("(entity1 == mergedEntity) ? " + (mergedEntity == entity1));
    System.out.println("(entity2 == mergedEntity) ? " + (mergedEntity == entity2));//entty1 is still detached
    transaction.commit(); //will cause an update to occur here
    newSession.close();
}
In the above code
  1. We have first created a detached entity(entity1) and further even modified it. 
  2. After that we have loaded a second entity (entity2). 
  3. Now to avoid the previous exception we call upon the merge operation on entity1. 
  4. The merge method returns the merged entity as another reference which we assigned to mergedEntity. The system displays the following on the console:
data is init
The data now loaded from db is mergeCase2
(entity1 == entity2) ? false
2797 [main] DEBUG org.hibernate.event.def.DefaultMergeEventListener  - merging d
etached instance
...
(entity1 == mergedEntity) ? false
(entity2 == mergedEntity) ? true
...
2860 [main] DEBUG org.hibernate.SQL  - 
    update
        Entity 
    set
        DATA=? 
    where
        id=?
As can be seen from the above Hibernate did not modify the detached entity. Instead it searched for the entity with same identifier in the persistence cache. On finding a match (entity2) it merged the data in entity1 into entitity2 and returned the reference to the existing persistent object. Object entity1 is still detached but the changes have been merged into entity2 - the object in session's PersistentCache.
As the data had been modified, Hibernate also schedules an update query on the object, thus reflecting the changes made to entity1 when it was in the detached state. If entity1 had no changes then the update query will not be fired.
But what would happen id we merged entity1 without having entity2 in the cache ?
public static  void mergeCase2() {
    Session session = sessionFactory.openSession();
    System.out.println("Fetching the object via get call");
    Entity entity1 = (Entity) session.get(Entity.class, 1);        
    String data = entity1.getData();
    session.close();
    System.out.println("data is " + data);
    entity1.setData("mergeCase2");

    Session newSession = sessionFactory.openSession();
    Transaction transaction = newSession.beginTransaction();        
    Entity mergedEntity = (Entity) newSession.merge(entity1); 
    //it searches in pc, not found -searches db - fetches dbentity, merges it with entity1 and returns dbentity
    System.out.println("(entity1 == mergedEntity) ? " + (mergedEntity == entity1));
    transaction.commit(); //will cause an update to occur here
    newSession.close();
}
The log displays the following:
data is init
2297 [main] DEBUG org.hibernate.event.def.AbstractSaveEventListener  - detached 
instance of: com.model.Entity
2297 [main] DEBUG org.hibernate.event.def.DefaultMergeEventListener  - merging d
etached instance
2297 [main] DEBUG org.hibernate.SQL  - 
    select
        entity0_.id as id0_0_,
        entity0_.DATA as DATA0_0_ 
    from
        Entity entity0_ 
    where
        entity0_.id=?
2328 [main] DEBUG org.hibernate.loader.Loader  - done entity load
(entity1 == mergedEntity) ? false
2344 [main] DEBUG org.hibernate.pretty.Printer  - com.model.Entity{id=1, data=me
rgeCase2}
2360 [main] DEBUG org.hibernate.SQL  - 
    update
        Entity 
    set
        DATA=? 
    where
        id=?
As can be seen from above, on call of merge
  1. Hibernate again checked its persistence cache. It did not find the object. 
  2. So it executed a database select to retrieve the entity with id 1 and added it to its persistence cache. 
  3. It then merged this new entity with the detached instance entity1 and detected the change in value for data. So after this it executed an update call ensuring that the change is persisted back to the database. In case of no change, the update query will not be executed.
Merge also handles the case where the object does not exist in the database at all
public static  void mergeCase3() {
    Entity entity1 = new Entity(); //new object created; in Transient state
    entity1.setData("mergeCase3");
    System.out.println("Creating a transient object ");
        
    Session newSession = sessionFactory.openSession();
    Transaction transaction = newSession.beginTransaction();        
    Entity mergedEntity = (Entity) newSession.merge(entity1);  
    //it searches in pc, will find no match, no match in db either
    // creates a new instance and schedules it for insertion, 
    //new entity merges it with entity1 and returns new entity
    System.out.println("(entity1 == mergedEntity) ? " + (mergedEntity == entity1));
    transaction.commit(); //will cause an insert to occur here
    newSession.close();
}
Here I actually created a new object and asked Hibernate to merge it. The logs are as below:
Creating a transient object 
2219 [main] DEBUG org.hibernate.event.def.DefaultMergeEventListener  - merging t
ransient instance
...
2281 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        Entity
        (DATA) 
    values
        (?)
(entity1 == mergedEntity) ? false
On calling merge,
  1. Hibernate sees that the record has no id - so it need not check in the persistence cache or the database. 
  2. Instead it schedules an insert call for the record adding it to the table and also to its persistence cache. Note however, that even in this case the merge method returns a different object than the one passed to it.
  3. Interestingly if I had used an assigned generator type here, Hibernate actually checks if the record is there in the persistence cache. Not finding it, it fires a select query on the database:
        select
            entity0_.id as id0_0_,
            entity0_.DATA as DATA0_0_ 
        from
            ENTITY entity0_ 
        where
            entity0_.id=?
    1082 [main] DEBUG org.hibernate.type.IntegerType  - binding '4' to parameter: 1
    ...
    1082 [main] DEBUG org.hibernate.loader.Loader  - done processing result set (0 r
    ows)
    ...
    1084 [main] DEBUG org.hibernate.event.def.DefaultMergeEventListener  - merging t
    ransient instance
    
    It is only after these steps that Hibernate goes ahead with the insert sql.
Thus in conclusion:
  1. Merge unlike update does not always fire an SQL update query. The entity will be updated only it has been modified.
  2. Merge unlike lock is capable of detecting changes made in the detached instance.
  3. If merge finds that the object is present in the first level cache, it  will use the same record. If not found in the cache, it will fetch it from the database.
  4. If the record does not exist at all, merge will schedule an insert for this record.
  5. The object returned by merge is always different to the one passed to it. This allows user to continue working wit the detached instance ( and possibly merge again) or user can discard the detached reference and work with the merged object)

12 comments:

  1. very good explaination with example . it helped me to understand what is exactly merge doing

    ReplyDelete
  2. Great explanation! the hibernate documentation should really have this.

    ReplyDelete
  3. great explanation... best regards from venezuela

    ReplyDelete
  4. Excellent explanation. No words to applause. Superb...

    ReplyDelete
  5. Nice! Helped me!

    ReplyDelete
  6. Thank you for this post! The only post which cleared my doubt between save() & merge()

    ReplyDelete
  7. Thanks, nice post

    ReplyDelete
  8. Very good post. Finally got answers to why merge fires select queries. It would have been good if there was a version of merge that took the entities at their face value to prevent select queries which take a lot of time for complex data models.

    ReplyDelete
  9. explanation was too good, but i need some real time use case of merge

    ReplyDelete
  10. Great post. Thank you very much !

    ReplyDelete