spring jpa 中事物管理的一个测试用例,看看嵌套事务的回滚机制

做测试的目的是为了搞清楚在spring 4.3.4 中的事务管理的一个简单嵌套会产生的结果

场景:

有一个service里面对A表进行插入操作,并且用@Transactional 进行事物管理。

同一个service里面对B表进行插入操作,并且也用@Transactional进行事物管理。

同时,根据业务需要,又要对这个2个表同时进行插入操作,并且也纳入一个事物操作,希望获得的结果:

全部成功后,提交2个表的插入

任何一个失败,2个表的插入操作都需要回滚。

相关代码如下

1. TestA表的EntityBean

package com.ninelephas.whale.entity;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQuery;
import javax.persistence.Table;

import lombok.Data;


/**
 * The persistent class for the TEST_A database table.
 * 
 */
@Data
@Entity
@Table(name = "TEST_A")
@NamedQuery(name = "TestA.findAll", query = "SELECT t FROM TestA t")
public class TestA implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    private String id;

    @Column(name = "name")
    private String name;

    @Column(name = "sex")
    private String sex;

    public TestA() {
        // do nothing
    }


}


2. TestB表的EntityBean

package com.ninelephas.whale.entity;

import java.io.Serializable;
import javax.persistence.*;

import lombok.Data;


/**
 * The persistent class for the TEST_B database table.
 * 
 */
@Data
@Entity
@Table(name = "TEST_B")
@NamedQuery(name = "TestB.findAll", query = "SELECT t FROM TestB t")
public class TestB implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    private String id;

    @Column(name = "name")
    private String name;

    @Column(name = "sex")
    private String sex;

    public TestB() {
        // do nothing
    }
}

3. TestA entityBean相关的japRepository类

package com.ninelephas.whale.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.ninelephas.whale.entity.TestA;

@Repository("com.ninelephas.whale.repository.ITestARepository")
public interface ITestARepository extends JpaRepository {

}


4. TestB entityBean相关的japRepository类

package com.ninelephas.whale.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.ninelephas.whale.entity.TestB;


@Repository("com.ninelephas.whale.repository.ITestBRepository")
public interface ITestBRepository extends JpaRepository {

}

5. 直接在当前类里面嵌套事物方法的service类

package com.ninelephas.whale.service.transaction.test;

import java.util.UUID;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.ninelephas.whale.entity.TestA;
import com.ninelephas.whale.entity.TestB;
import com.ninelephas.whale.repository.ITestARepository;
import com.ninelephas.whale.repository.ITestBRepository;
import com.ninelephas.whale.service.ServiceException;

/**
 * @ClassName: TransactionEmbed
 * @Description: 单独和合并事物功能的测试
 * @author 徐泽宇
 * @date 2016年11月22日 下午4:26:29
 *
 */
@Service("com.ninelephas.whale.service.transaction.test.TransactionEmbed")

public class TransactionEmbed {
    /**
     * Logger for this class
     */
    private static final Logger logger = LogManager.getLogger(TransactionEmbed.class.getName());

    @Autowired
    private ITestARepository iTestARepository;

    @Autowired
    private ITestBRepository iTestBRepository;
    
    /**
     * addTestA
     * 
     * @Auther 徐泽宇
     * @Date 2016年11月22日 下午4:26:09
     * @Title: addTestA
     * @Description: 测试插入testa表的事物
     */
    @Transactional
    public void addTestA() {
        if (logger.isDebugEnabled()) {
            logger.debug("addTestA() - start"); //$NON-NLS-1$
        }

        TestA testA = new TestA();
        testA.setName("testa");
        testA.setId(UUID.randomUUID().toString());
        testA.setSex("male");
        this.iTestARepository.save(testA);

        if (logger.isDebugEnabled()) {
            logger.debug("addTestA() - end"); //$NON-NLS-1$
        }
    }

    /**
     * addTestB
     * 
     * @Auther 徐泽宇
     * @Date 2016年11月22日 下午4:26:09
     * @Title: addTestA
     * @Description: 测试插入testb表的事物
     * @param isThrowsException
     * @throws ServiceException
     */
    @Transactional
    public void addTestB(boolean isThrowsException) throws ServiceException {
        if (logger.isDebugEnabled()) {
            logger.debug("addTestB() - start"); //$NON-NLS-1$
        }

        TestB testB = new TestB();
        testB.setName("testa");
        testB.setId(UUID.randomUUID().toString());
        testB.setSex("male");
        if (isThrowsException) {
            throw new ServiceException("有意抛出的例外");
        } else {
            this.iTestBRepository.save(testB);
        }
        if (logger.isDebugEnabled()) {
            logger.debug("addTestB() - end"); //$NON-NLS-1$
        }
    }

    /**
     * TranactionalCombined
     * 
     * @Auther 徐泽宇
     * @Date 2016年11月22日 下午5:14:52
     * @Title: TranactionalCombined
     * @Description: 调用自己的2个事务方法,第二个事务方法会抛出错误
     * @throws ServiceException
     */
    @Transactional()
    public void secondMethodError() throws ServiceException {
        this.addTestA();
        this.addTestB(true);
    }


    /**
     * allSucess
     * 
     * @Auther 徐泽宇
     * @Date 2016年11月23日 下午6:18:27
     * @Title: allSucess
     * @Description: 2个都会成功执行的 单独事务 同时调用
     * @throws ServiceException
     */
    @Transactional()
    public void allSucess() throws ServiceException {
        this.addTestA();
        this.addTestB(false);
    }
}



6. 另外一个service类,调用前面类里面的事物方法

package com.ninelephas.whale.service.transaction.test;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.ninelephas.whale.service.ServiceException;

/**
 * @ClassName: TransactionCombined
 * @Description: 单独和合并事物功能的测试
 * @author 徐泽宇
 * @date 2016年11月22日 下午4:26:29
 *
 */
@Service("com.ninelephas.whale.service.transaction.test.TransactionCombined")

public class TransactionCombined {
    /**
     * Logger for this class
     */
    private static final Logger logger = LogManager.getLogger(TransactionCombined.class.getName());

    @Autowired
    private TransactionEmbed transactionEmbed;

    
    /**
      * TranactionalCombined
      * 
      * @Auther 徐泽宇
      * @Date   2016年11月22日 下午5:14:52
      * @Title: TranactionalCombined
      * @Description: 调用另外一个service的2个事物方法,都成功
      * @throws ServiceException
      */
    @Transactional
    public void allSucess() throws ServiceException{
        if (logger.isDebugEnabled()) {
            logger.debug("tranactionalCombined() - start"); //$NON-NLS-1$
        }

        this.transactionEmbed.addTestA();
        this.transactionEmbed.addTestB(false);

        if (logger.isDebugEnabled()) {
            logger.debug("tranactionalCombined() - end"); //$NON-NLS-1$
        }
    }
    
    /**
      * secondMethodError
      * 
      * @Auther 徐泽宇
      * @Date   2016年11月23日 下午6:48:46
      * @Title: secondMethodError
      * @Description: 调用另外一个service里面的2个事物方法,第二个抛出错误
      * @throws ServiceException
      */
    @Transactional
    public void  secondMethodError()throws ServiceException{
        if (logger.isDebugEnabled()) {
            logger.debug("secondMethodError() - start"); //$NON-NLS-1$
        }
        try{
            this.transactionEmbed.addTestA();
            this.transactionEmbed.addTestB(true);
        }catch (ServiceException e) {
            logger.error(e.getMessage());
            throw e;
        }
        

        if (logger.isDebugEnabled()) {
            logger.debug("secondMethodError() - end"); //$NON-NLS-1$
        }
    }


}



7. 测试用例

package com.ninelephas.whale.tranactional;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import com.ninelephas.whale.BaseTest;
import com.ninelephas.whale.service.transaction.test.TransactionCombined;
import com.ninelephas.whale.service.transaction.test.TransactionEmbed;

/**
 * @ClassName: CombinedTranactional
 * @Description: 测试事物联合的功能
 * @author 徐泽宇
 * @date 2016年11月22日 下午4:51:45
 *
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:/META-INF/applicationContext.xml", "classpath:/META-INF/spring-mvc.xml"})
@WebAppConfiguration
public class CombinedTranactional extends BaseTest {
    /**
     * Logger for this class
     */
    private static final Logger logger = LogManager.getLogger(CombinedTranactional.class.getName());

    @Autowired
    private TransactionEmbed transactionEmbed;
    
    @Autowired
    private TransactionCombined  transactionCombined;

    /**
     * testTranactionalCombined
     * 
     * @Auther 徐泽宇
     * @Date 2016年11月22日 下午5:01:57
     * @Title: testTranactionalEmbed
     * @Description: 测试:调用一个service的方法,这个方法调用service自身的2个事物方法。2个事物都成功的状况下
     * 结果:2个表都增加了一条记录
     * @throws Exception
     */
    @Test
    public void allSucessEmbed() throws Exception {
        transactionEmbed.allSucess();
    }
    
    /**
     * testTranactionalCombined
     * 
     * @Auther 徐泽宇
     * @Date 2016年11月22日 下午5:01:57
     * @Title: testTranactionalCombined
     * @Description: 测试:调用一个service的方法,这个方法调用另外一个service类里面的2个事物方法。对2个表进行增加数据
     * 结果:2个表都增加了一条记录
     * @throws Exception
     */
    @Test
    public void allSucessCombined() throws Exception {
        transactionCombined.allSucess();
    }
    
    /**
     * testTranactionalCombined
     * 
     * @Auther 徐泽宇
     * @Date 2016年11月22日 下午5:01:57
     * @Title: testTranactionalCombined
     * @Description: 测试:调用一个service的方法,这个方法调用service自身的2个事物方法。第二个事物失败的情况下
     * 如果运行错误: 前面一个表插入了记录,后面一个没有插入
     * 如果运行正确:  2个表都没插入记录
     * @throws Exception
     */
    @Test
    public void secondMethodErrorEmbed() throws Exception {
        transactionEmbed.secondMethodError();
    }
    
    /**
     * secondMethodErrorCombined
     * 
     * @Auther 徐泽宇
     * @Date 2016年11月22日 下午5:01:57
     * @Title: testTranactionalCombined
     * @Description: 测试:调用一个service的方法,这个方法调用另外一个service的2个事物方法。第二个事物失败的情况下
     * 如果运行错误: 前面一个表插入了记录,后面一个没有插入
     * 如果运行正确:  2个表都没插入记录
     * @throws Exception
     */
    @Test
    public void secondMethodErrorCombined() throws Exception {
        if (logger.isDebugEnabled()) {
            logger.debug("secondMethodErrorCombined() - start"); //$NON-NLS-1$
        }

        transactionCombined.secondMethodError();

        if (logger.isDebugEnabled()) {
            logger.debug("secondMethodErrorCombined() - end"); //$NON-NLS-1$
        }
    }
}

8. Web上用于测试的Controller
package com.ninelephas.whale.controller.test;

import java.io.IOException;

import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

import com.ninelephas.whale.controller.ControllerException;
import com.ninelephas.whale.enumeration.AccessoryBucket;
import com.ninelephas.whale.service.ServiceException;
import com.ninelephas.whale.service.accessory.AccessoryService;
import com.ninelephas.whale.service.transaction.test.TransactionCombined;
import com.ninelephas.whale.service.transaction.test.TransactionEmbed;

/**
 * 
 * @ClassName: TestController
 * @Description: 测试Controller ,用于测试一些service功能是否正常
 * @author 白亮
 * @date 2016年10月25日 下午3:59:01
 *
 */

@Controller("com.ninelephas.whale.controller.test")
@RequestMapping(value = "/test")
public class TestController {
    /**
     * Logger for this class
     */
    private static final Logger logger = LogManager.getLogger(TestController.class.getName());


    @Autowired
    private AccessoryService accessoryService;

    @Autowired
    private TransactionEmbed transactionEmbed;

    @Autowired
    private TransactionCombined transactionCombined;
    


    /**
     * allSuccessEmbed
     * 
     * @Auther 徐泽宇
     * @Date 2016年11月23日 下午11:58:44
     * @Title: allSuccess
     * @Description: 测试:调用一个service的方法,这个方法调用service自身的2个类事物方法。2个事物都成功的状况下
     * @return
     * @throws ControllerException
     */
    @RequestMapping(value = "tran_all_success_embed")
    @ResponseBody
    public String allSuccessEmbed() throws ControllerException {
        try {
            transactionEmbed.allSucess();
        } catch (ServiceException error) {
            logger.error(error, error.fillInStackTrace());
            throw new ControllerException(error.getMessage());
        }
        return "ok";
    }

    
    /**
     * allSuccessCombined
     * 
     * @Auther 徐泽宇
     * @Date 2016年11月23日 下午11:58:44
     * @Title: allSuccess
     * @Description: 测试:调用一个service的方法,这个方法调用另外一个service的2个类事物方法。2个事物都成功的状况下
     * @return
     * @throws ControllerException
     */
    @RequestMapping(value = "tran_all_success_combined")
    @ResponseBody
    public String allSuccessCombined() throws ControllerException {
        try {
            transactionEmbed.allSucess();
        } catch (ServiceException error) {
            logger.error(error, error.fillInStackTrace());
            throw new ControllerException(error.getMessage());
        }
        return "ok";
    }


    /**
     * secondMethodErrorEmbed
     * 
     * @Auther 徐泽宇
     * @Date 2016年11月24日 上午12:06:02
     * @Title: secondMethodErrorEmbed
     * @Description: 测试:调用一个service的方法,这个方法调用service自身的2个类事物方法。第二个事物失败的情况下
     * @return
     * @throws ControllerException
     */
    @RequestMapping(value = "tran_second_method_error_embed")
    @ResponseBody
    public String secondMethodErrorEmbed() throws ControllerException {
        try {
            transactionEmbed.secondMethodError();
        } catch (ServiceException error) {
            logger.error(error, error.fillInStackTrace());
            throw new ControllerException(error.getMessage());
        }
        return "ok";
    }
    
    /**
     * secondMethodErrorCombined
     * 
     * @Auther 徐泽宇
     * @Date 2016年11月24日 上午12:06:02
     * @Title: secondMethodErrorEmbed
     * @Description: 测试:调用一个service的方法,这个方法调用service自身的2个类事物方法。第二个事物失败的情况下
     * @return
     * @throws ControllerException
     */
    @RequestMapping(value = "tran_second_method_error_combined")
    @ResponseBody
    public String secondMethodErrorCombined() throws ControllerException {
        try {
            transactionCombined.secondMethodError();
        } catch (ServiceException error) {
            logger.error(error, error.fillInStackTrace());
            throw new ControllerException(error.getMessage());
        }
        return "ok";
    }

}



测试结果:

1. 如果表的插入动作用@Transactional注解,那么无论在同一个service类,还是其他的service类里面,都不会产生后面一个方法出错,全部回滚的情况。而是事物都会单独提交。

2. 如果在JUnit的类里面的测试方法上加入@Transactional注解。无论有多少个事物嵌套,都会表现为一个事物。后面一个出错,全部回滚,这个是我们希望的情况。


?????Why  ?????

原因: spring默认的回滚策略是:发生异常不一定回滚,只有发生运行时异常才回滚

解决办法: 

1. service类中抛出的ServiceException.java 去继承   RuntimeException.java 

2. 也可以在配置文件中对具体异常类型的回滚策略进行控制,并且前面带"+"表示提交,带"-"表示回滚。


3. 在嵌套的事物方法上用如下语句来注解事物,让Exception.class 来回滚事物

@Transactional(rollbackFor={ServiceException.class})

或者

@Transactional(rollbackFor={Exception.class})


相关阅读:


官方文档: http://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html


另外一篇启发我的博文  http://www.importnew.com/19489.html


声明式事务是spring处理事务的标志性方式,它是在 TransactionDefinition 接口中定义各种各样的事务属性,然后通过TransactionProxyFactoryBean类或者TransactionInterceptor类或者标签来在配置文件中进行事务属性在目标对象目标方法上的配置声明,以供 PlatfromTransactionManager使用,PlatfromTransactionManager  spring 事务管理的核心接口,不同的持久化技术各自都有它的实现类,如hibernate的就是HibernateTransactionManager。 声明式事务的好处是,使 我们从复杂的事务处理中得到解脱。使得我们再也无需去处理获得连接、关闭连接、事务提交和回滚等这些操作。再也无需我们在与事务相关的方法中处理大量的  try    catch    finally  代码。
    事务属性通常由事务的隔离级别,传播属性,事务的超时值,事务的只读标志以及事务的回滚策略组成。
 
1.事务的隔离级别:
  ISOLATION_DEFAULT:这是一个PlatfromTransactionManager默认的隔离级别,使用后台数据库默认的事务隔离级别.
  ISOLATION_READ_UNCOMMITTED:这是事务最低的隔离级别,它允许你读取未提交的数据,这种隔离级别会产生脏读,不可重复读和幻像读。
  ISOLATION_READ_COMMITTED: 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。可以防止脏读,但是还可能发生不可重复读和幻读。
  ISOLATION_REPEATABLE_READ: 这种事务隔离级别除了保证一个事务不能读取另一个事务未提交的数据外,还保证了一个事务对相同字段的多次读取结果都是一致的,除非数据被事务本身改变。 可以防止脏读,不可重复读。但是可能出现幻像读。
   ISOLATION_SERIALIZABLE:这是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。但是它的代价也是最高的。
 
2.事务的传播特性:
   PROPAGATION_REQUIRED: 表示当前方法必须运行在一个事务中。如果运行环境中已有一个事务在运行,该方法就运行在这个事务中,否则,就要开始一个新的事务。
   PROPAGATION_SUPPORTS:表示当前方法支持但不需要事务处理环境。如果运行环境中已有一个事务在运行,该方法就运行在这个事务中,否则,该方法就在非事务环境下运行。
   PROPAGATION_MANDATORY:表示该方法强制性的运行在事务环境中。如果运行环境中已有一个事务在运行,该方法就运行在这个事务中,否则,就抛出异常。
   PROPAGATION_REQUIRES_NEW:表示该方法总是运行在它自己的事务中。如果运行环境中已有一个事务在运行,则将原来事务挂起。
   PROPAGATION_NOT_SUPPORTED:表示该方法总是运行在非事务环境中。如果运行环境中已有一个事务在运行,则将原来的事务挂起。
   PROPAGATION_NEVER:表示该方法总是运行在非事务环境中。如果运行环境中已有一个事务在运行,则将抛出异常。
   PROPAGATION_NESTED:表示该方法可以运行在嵌套事务中。如果运行环境中已有一个事务在运行,则该方法运行在一个嵌套事务中。运行在嵌套事务中的方法可以从当前事务中单独的进行提交或回滚。否则,它跟PROPAGATION_REQUIRED的性质是一样的。
 
3.事务的只读标志:
  readOnly:由于事务操作最终都是由后台数据库来具体实施的,配置了readOnly的方法在进行数据库操作时会采取一些优化措施,比如避免脏数据检查等。注意,如果使用hibernate,还要把该方法的flush模式设置成FLUSH_NEVER,如:template.setFlushMode(HibernateTemplate.FLUSH_NEVER);
   另外,我们不难发现,只有将那些可能产生新事务的方法的事务配置成readOnly才有意义(传播属性为PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED)。
   spring默认没有只读标志。
 
4.事务的超时策略
   假设你的事务在预料之外长时间运行,因为事务可能设计对后台数据库的锁定,长时间运行的事务会不必要的占用数据库自愿。与其等它结束,不如声明一个事务,在指定时间内不结束就自动回滚。
   由于超时策略是在事务启动时就开始计时,所以也只有将那些可能产生新事务的方法的事务配置成readOnly才有意义(传播属性为PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED)。
 
5.事务的回滚策略:
  spring默认的回滚策略是:发生异常不一定回滚,只有发生运行时异常才回滚。但是,我们可以在配置文件中对具体异常类型的回滚策略进行控制,并且前面带"+"表示提交,带"-"表示回滚。






你可能感兴趣的:(java,spring)