Search code examples
springhibernatejpatransactional

Behaviour of Hibernate merge() when @Transactional method throws an exception


I would like to ask the reasoning of this behaviour as it seems I do not fully understand the differences between persist() and merge() in Hibernate when running into Spring @Transactional methods/classes.

I have the following code which is supposed to rollback the DB operation but it doesn't (the whole class is annotated as @Transactional):

@Override
public MyBean assignNewFoo(Integer id, Integer idNewFoo) {

    MyBean bean = myBeanRepository.findOne(id);
    bean = myBeanRepository.save(bean);

    bean.setNewFoo(
            fooManagement.findById(idNewFoo)
            );
    if (true) throw new RuntimeException();
    return bean;
}

The following code does rollback as expected when an exception is thrown:

@Override
public MyBean assignNewFoo(Integer id, Integer idNewFoo) {

    MyBean bean = myBeanRepository.findOne(id);
    myBeanRepository.save(bean);

    bean.setNewFoo(
            fooManagement.findById(idNewFoo)
            );
    if (true) throw new RuntimeException();
    return bean;
}

The save() method comes from the class org.springframework.data.jpa.repository.support.SimpleJpaRepository, so its code is:

@Transactional
public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

The entity is an existing one, so I understand it's doing a merge(). As per JPA specification:

The find method (provided it is invoked without a lock or invoked with LockModeType.NONE) and the getReference method are not required to be invoked within a transaction context. If an entity manager with transaction-scoped persistence context is in use, the resulting entities will be detached; if an entity manager with an extended persistence context is used, they will be managed.


The merge operation allows for the propagation of state from detached entities onto persistent entities managed by the entity manager. The semantics of the merge operation applied to an entity X are as follows:

  • If X is a detached entity, the state of X is copied onto a pre-existing managed entity instance X' of the same identity or a new managed copy X' of X is created.
  • If X is a new entity instance, a new managed entity instance X' is created and the state of X is copied into the new managed entity instance X'.
  • If X is a removed entity instance, an IllegalArgumentException will be thrown by the merge operation (or the transaction commit will fail).
  • If X is a managed entity, it is ignored by the merge operation, however, the merge operation is cascaded to entities referenced by relationships from X if these relationships have been annotated with the cascade element value cascade=MERGE or cascade=ALL annotation.
  • For all entities Y referenced by relationships from X having the cascade element value cascade=MERGE or cascade=ALL, Y is merged recursively as Y'. For all such Y referenced by X, X' is set to reference Y'. (Note that if X is managed then X is the same object as X'.)
  • If X is an entity merged to X', with a reference to another entity Y, where cascade=MERGE or cascade=ALL is not specified, then navigation of the same association from X' yields a reference to a managed object Y' with the same persistent identity as Y.
  1. If the returned copy by merge() is supposedly the managed entity, why are changes stored in the DB when I use the detached one? (unless there is an exception. This is the behaviour I want)

  2. Why changes are committed anyway if I modify the new managed entity but an exception is thrown?

EDIT As requested by @alan-hay:

package org.customer.somefoos.service.impl;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import org.customer.somefoos.entity.MyBean;
import org.customer.somefoos.repository.MyBeanRepository;
import org.customer.somefoos.service.MyBeanManagement;
import org.customer.somefoos.service.FooManagement;

@Service
@Transactional
public class MyBeanManagementImpl implements MyBeanManagement {

    @Resource
    private MyBeanRepository myBeanRepository;

    @Resource
    private FooManagement fooManagement;


    @Override
    public List<MyBean> findAll() {
        return myBeanRepository.findAll();
    }

    @Override
    public MyBean findById(Integer id) {
        return myBeanRepository.findOne(id);
    }

    @Override
    public void delete(Integer id) {
        myBeanRepository.delete(id);
    }

    @Override
    public MyBean save(MyBean myBean) { 
        return myBeanRepository.save(myBean);
    }

    @Override
    public MyBean assignNewFoo(Integer id, Integer idNewFoo) {

        MyBean bean = myBeanRepository.findOne(id);
        myBeanRepository.save(bean);

        bean.setNewFoo(
                fooManagement.findById(idNewFoo)
                );
        if (true) throw new RuntimeException();
        return bean;
    }

}

Solution

    1. It seems that you misunderstand merge semantics and container-managed transactions behaviour. Your assignNewFoo method is transactional and your 'bean' instance is loaded from the repository. Because of this, 'bean' instance keeps being managed until transaction ends (or until you remove in from a persistence context manually). This means that myBeanRepository.save(bean); call does nothing as 'bean' is already a JPA-managed entity. myBeanRepository.save(bean) == bean will be true as long as saving is performed in same transaction 'findOne' has been issued in. Merge is used to apply changes made to a non-managed instance of an entity to a managed one. This code illustrates the case merge is being used for:

      MyBean bean = repo.findOne(id);
      MyBean anotherInstance = new MyBean();
      anotherInstance.setId(id);
      anotherInstance.setNewFoo("100");
      MyBean managed = repo.save(anotherInstance);
      // And now we take a look:
      managed == bean; // => true
      anotherInstance == managed; // => false
      bean.getNewFoo(); // => "100"
      // An anotherInstance is still detached while save() call has 
      // returned us a managed instance ('bean')
      

      As per JPA Spec entry you reference to: it's inapplicable here. It says about non-transactional searches, but your search is performed within transaction started by assignNewFoo invocation.

    2. From all the stuff written above: two of your code samples provided to demonstrate no-rollback behaviour are, in fact, identical. There are some reasons you may face the issue you complain about:

      • You're calling assignNewFoo from a @Transactional method and you peform transaction application checks in this outer @Transactional method. Since your propagation level is 'REQUIRED' and RuntimeException is not caught inside assignNewFoo invocation, the transaction will be marked for rollback once assignNewFoo invocation is finished, but actual rollback will be performed upon completion of method your transaction has been propagated from.
      • If you're 100 % sure you've done everything right, this might be a Spring/Provider/DBMS issue. I was unable to reproduce this bug on latest Spring Boot + Hibernate 4 + HSQLDB, it may worth checking if you're out of options.