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<TestA, String> {

}


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<TestB, String> {

}

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類或者<tx>和<aop>標籤來在配置文件中進行事務屬性在目標對象目標方法上的配置聲明,以供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默認的回滾策略是:發生異常不一定回滾,只有發生運行時異常纔回滾。但是,我們可以在配置文件中對具體異常類型的回滾策略進行控制,並且前面帶"+"表示提交,帶"-"表示回滾。






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章