SpringBoot系列: JdbcTemplate 事務控制


============================
Spring JdbcTemplate 事務控制
============================
之前使用 JDBC API 操作, 經常用到的對象有: connection 和 preparedStatement.
dbConnection.setAutoCommit(false); //transaction block start
//some db manipulation
dbConnection.commit(); //transaction block end

雖然通過 jdbcTemplate 可以獲取 connection 對象, 但是我們不能使用 jdbcTemplate.getDataSource().getConnection().rollback() 命令式方式來控制事務, 因為這樣獲取到 connection 不並一定是執行SQL的那個connection.

Spring提供下面兩個方式控制事務:
1. 命令式事務控制方式.
使用 TransactionTemplate 類.
特點: 個人覺得 JdbcTemplate + TransactionTemplate 非常搭配, 都是輕量級, 都是命令式. 另外 TransactionTemplate 因為是寫代碼形式, 事務控制做到更細粒度.
2. 聲明式事務控制方式 (@Transactional)
將DB訪問封裝到 @Service/@Component 類中, 並將具體訪問過程放到一個 public 方法中, 並加上 @Transactional 注解.
優點: 代碼很簡潔, 不僅適用於 JdbcTemplate, 而且適用於 Jpa/MyBatis 等數據庫訪問技術.
缺點: 事務控制粒度較粗, 只能做到函數粒度的事務控制, 無法做到代碼塊級的事務控制, 另外需要理解其背后是通過 AOP + proxy 方式實現的, 使用有比較多的講究, 下文有提及.


============================
Spring 事務控制的基礎
============================
Spring 控制方式基礎是 PlatformTransactionManager 接口, 它為各種數據訪問技術提供了統一的事務支持接口, 不同的數據技術都有自己的實現:
   Spring JDBC 技術: DataSourceTransactionManager
   JPA 技術: JpaTransactionManager
   Hibernate 技術: HibernateTransactionManager
   JDO 技術: JdoTransactionManager
   分布式事務: JtaTransactionManager

Spring Boot 項目中, 引入了 spring-boot-starter-jdbc 之后, 會自動注入一個 DataSourceTransactionManager 類型 bean 對象, 這個對象有兩個名稱, 分別為 transactionManager 和 platformTransactionManager .
引入了 spring-boot-starter-data-jpa 依賴后, 會自動注入一個 JpaTransactionManager 類型 bean 對象, 這個對象有兩個名稱, 分別為 transactionManager 和 platformTransactionManager.

如果我們項目有多個數據源, 或者既引入了 spring-boot-starter-jdbc, 又引入了 spring-boot-starter-data-jpa 依賴, 自動注入事務控制器就會混亂, 所以需要創建一個 TransactionManager configuration 類, 手動為不同數據源建立對應的 PlatformTransactionManager bean. 如果使用 @Transactional 注解控制事務, 需要指定對應的事務控制器, 比如 @Transactional(value="txManager1") .

@EnableTransactionManagement
public class TransactionManagerConfig{
    
    @Bean    
    @Autowired    //自動注入 dataSource1
    public PlatformTransactionManager txManager1(DataSource dataSource1) {
        return new DataSourceTransactionManager(dataSource1);
    }
    
    @Bean 
    @Autowired    //自動注入 dataSource2
    public PlatformTransactionManager txManager2(DataSource dataSource2) {
        return new DataSourceTransactionManager(dataSource2);
    }    
}

 

============================
使用 TransactionTemplate 進行事務控制
============================
生成 TransactionTemplate 對象時, 需要指定一個 Spring PlatformTransactionManager 接口的實現類.
因為我們使用的是 JdbcTemplate, 所以創建 TransactionTemplate 對象要傳入 DataSourceTransactionManager 參數.
使用 TransactionTemplate 類控制事務, 我們只需要將數據訪問代碼封裝成一個callback對象, 然后將callback對象傳值給TransactionTemplate.execute()方法, 事務控制由TransactionTemplate.execute()完成.

TransactionTemplate.execute() 函數的主要代碼:

public <T> T execute(TransactionCallback<T> action) throws TransactionException {
    TransactionStatus status = this.transactionManager.getTransaction(this);
    T result;
    try {
        result = action.doInTransaction(status);  //
    }
    catch (RuntimeException | Error ex) {
        // Transactional code threw application exception -> rollback
        rollbackOnException(status, ex);
        throw ex;
    }
    catch (Throwable ex) {
        // Transactional code threw unexpected exception -> rollback
        rollbackOnException(status, ex);
        throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
    }
    this.transactionManager.commit(status);
    return result;         
}


從上面代碼中可以看到, 要想要回滾數據庫操作, 可以在callback對象的doInTransaction函數拋出異常, 或者在doInTransaction函數中可以控制 一個 TransactionStatus 接口的變量(transactionStatus 變量), 該TransactionStatus 接口為處理事務的代碼提供一個簡單的控制事務執行和查詢事務狀態的方法,  調用 transactionStatus.setRollbackOnly() 可以回滾事務. 

TransactionTemplate.execute() 使用回調機制傳參, 參數類型是 TransactionCallback<T> 接口, 實參可以是:
1. TransactionCallbackWithoutResult 類實例, 適合於事務沒有返回值, 例如save、update、delete等等.
2. TransactionCallback<T> 虛擬類實例, TransactionCallback<T> 泛型中的類型 T 是 doInTransaction() 函數的返回類型, 一般情況下這個 T 類型並不是很重要的, 直接使用 Object 類型即可. 也可以使用將Select的結果保存到這個返回值上.

 

 

==========================
使用 @Transactional 注解
==========================
使用 @Transactional 的要點有:
1. 在DAO 層使用 JdbcTemplate 實現DB操作, 在 Service 的實現類上加上 @Transactional 注解, 不推薦在 Service 接口上加 @Transactional 注解.
2. 需要進行事務控制的方法, 必須是 public 方法, 同時要打上 @Transactional 注解.
3. 也可以在Class上加上 @Transactional 注解, 這樣相當於給每個 public 函數加上了 @Transactional 注解, 當然我們還可以在其中的函數上加該注解, 這時候將以函數上的設置為准.

 

@Transactional 使用陷阱:
1. 只有 public 方法打上 @Transactional 注解, 事務控制才能生效.
2. 注意自調用問題, @Transactional 注解僅在外部類的調用才生效, 原因是使用 Spring AOP 機制造成的. 所以: 主調函數如果是本Service類, 應該也要打上 @Transactional, 否則事務控制被忽略.
3. 缺省的情況下, 只有 RuntimeException 類異常才會觸發回滾. 如果在事務中拋出其他異常,並期望回滾事務, 必須設定 rollbackFor 參數.
     例子: @Transactional(propagation=Propagation.REQUIRED,rollbackFor= MyException.class)
4. 如果主調函數和多個被調函數都加了 @Transactional 注解, 則整個主調函數將是一個統一的事務控制范圍, 甚至它們分屬多個Service也能被統一事務控制着
5. 通常我們應該使用 Propagation.REQUIRED, 但需要說明的是, 如果一個非事務方法順序調用了"兩個不同service bean"的事務函數, 它們並不在同一個事務上下文中, 而是分屬於不同的事務上下文.

 

 

關於自調用問題和 Public 的限制, 是因為Spring 使用了 Spring AOP 代理造成的, 如果要解決這兩個問題, 使用 AspectJ 取代 Spring AOP 代理. 但並不推薦這么做, 更換底層AOP技術可能會引起其他副作用. 

 

示例: 單個 Service 類的多個事務函數調用問題

Class ServiceImpl{
    @Autowired
    Dao dao;
    
    // 因為自調用問題, 直接調用 test() 將沒有任何事務控制
    public void test() {    
        test1();
        test2();        
    }
    
    // 因為 testNew() 加了 @Transactional 注解, 所以形成了一個整體事務. 
    @Transactional 
    public void testNew() {    
        test1();
        test2();        
    }    

    @Transactional
    public void test1() {
        dao.updateUser('1') ;
    }

    @Transactional
    public void test2() {
        dao.updateUser('2') ;
    }
}

 

示例: 多個 Service 類的事務函數調用問題, 

 

Class ServiceImplA{  
    @Autowired
    Dao1 dao;
    
    @Transactional
    public void test() {
        dao.updateUser('1') ;   
    } 
}

Class ServiceImplB{  
    @Autowired
    Dao2 dao;
        
    @Transactional
    public void test() {
        dao.updateOrder('2') ;  
    } 
}

Class Controller{
    @Autowired
    ServiceImplA serviceImplA;

    @Autowired
    ServiceImplB serviceImplB;
    
    //serviceImplA.test() 和 serviceImplB.test() 並不是在同一個事務上下文中, 他們分別在各自的事務上下文中.
    //原因是: 事務上下文是從屬於主調bean的, 不同主調bean的事務是在不同的事務上下文中. 
    public void wholeTest() {
       serviceImplA.test();
       serviceImplB.test();
    } 
}

 

 

============================
pom.xml & application.properties & DB
============================

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

 

application.properties文件

#application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/world?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=toor
spring.datasource.driver-class-name=com.mysql.jdbc.Driver


在示例中使用了MySQL 官方提供的 sakila 樣本數據庫, 該數據用來模擬DVD租賃業務.
先 clone 一個actor_new 新表.

CREATE TABLE `actor_new` (
  `actor_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) NOT NULL,
  `last_name` varchar(45) NOT NULL,
  `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`actor_id`),
  KEY `idx_actor_last_name` (`last_name`)
) ENGINE=InnoDB AUTO_INCREMENT=201 DEFAULT CHARSET=utf8

insert into actor_new  select * from actor ;
 


==========================
使用 TransactionTemplate 的java 代碼
==========================
使用 TransactionTemplate 很直接, 不需要將代碼先封裝為class, 將我們的JdbcTemplate代碼以匿名類的形式嵌入到 transTemplate.execute() 方法即可.

package com.example.demo;

import java.sql.SQLException;

import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;


@SpringBootApplication
public class TransTemplateApplication implements CommandLineRunner {
    @Autowired
    DataSource dataSource;

    @Autowired
    JdbcTemplate jdbcTemplate;

    TransactionTemplate transTemplate;

    /*
     * 該方法會被Spring自動在合適的時機調用, 用來初始化一個 TransactionTemplate 對象. 參數 dataSource 被自動注入.
     */
    @Autowired
    private void transactionTemplate(DataSource dataSource) {
        transTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource));
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(TransTemplateApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        runTransactionSamples();
    }


    /*
     * 帶有事務控制的DML 將DML操作放到 TransactionCallback類的doInTransaction()方法中.
     * 只有在下面兩種情況下才會回滾:
     * 1. 通過設置 transactionStatus 為 RollbackOnly
     * 2. 拋出任何異常
     */
    public void runTransactionSamples() throws SQLException {

        transTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus transactionStatus) {
                // DML執行
                jdbcTemplate.update("Delete from actor_new where actor_id=?", 11);

                // 回滾
                transactionStatus.setRollbackOnly();
                return null;
            }
        });

    }
}

 

==========================
使用 @Transactional 注解的 java 代碼
==========================
使用 @Transactional , 需要進行事務控制的方法, 必須是 public 方法, 同時要打上 @Transactional 注解.

 

package com.example.demo;

import java.sql.SQLException;

import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


@SpringBootApplication
public class AnnotationSampleApplication implements CommandLineRunner {

    @Autowired
    ActorService actorService;

    public static void main(String[] args) throws Exception {
        SpringApplication.run(AnnotationSampleApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        //actorService.runRollbackCase();
        actorService.runCommitCase();
    }
}

/*
 * 自定義 RuntimeException 類
 * */
class MyRuntimeException extends RuntimeException {
    private static final long serialVersionUID = -862367066204594182L;

    public MyRuntimeException(String msg) {
        super(msg);
    }
}

/*
 * 自定義 非RuntimeException 類
 * */
class MyException extends Exception {
    private static final long serialVersionUID = 8175536977672815349L;

    public MyException(String msg) {
        super(msg);
    }
}

@Service
class ActorService {
    @Autowired
    DataSource dataSource;

    @Autowired
    JdbcTemplate jdbcTemplate;

    /*
     * 回滾事務的示例 -- 能回滾
     */
    @Transactional
    public void runRollbackCase1() throws SQLException {
        jdbcTemplate.update("Delete from actor_new where actor_id=?", 15);

        throw new RuntimeException("故意拋出異常來回滾事務.");
    }

    /*
     * 回滾事務的示例 -- 拋出 Non-RuntimeException 異常, 事務不能回滾
     */
    @Transactional
    public void runRollbackCase2() throws SQLException, MyException {
        jdbcTemplate.update("Delete from actor_new where actor_id=?", 13);
        throw new MyException("故意拋出異常來回滾事務.");
    }

    /*
     * 回滾事務的示例 -- 拋出MyException異常, 並設置了 rollbackFor 參數, 事務能回滾
     */
    @Transactional(rollbackFor=MyException.class)
    public void runRollbackCase3() throws SQLException, MyException {
        jdbcTemplate.update("Delete from actor_new where actor_id=?", 14);
        throw new MyException("故意拋出異常來回滾事務.");
    }

    /*
     * 回滾事務的示例 -- 拋出自定義RuntimeException異常, 事務能回滾
     */
    @Transactional
    public void runRollbackCase4() throws SQLException {
        jdbcTemplate.update("Delete from actor_new where actor_id=?", 14);
        throw new MyRuntimeException("故意拋出異常來回滾事務.");
    }

    /*
     * 回滾事務的示例 -- 方法最后沒有拋出異常, 不回滾
     */
    @Transactional
    public void runRollbackCase5() throws SQLException {
        jdbcTemplate.update("Delete from actor_new where actor_id=?", 14);
        try {
            throw new MyRuntimeException("故意拋出異常來回滾事務.");
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
        }
    }

    /*
     * runRollbackCase6 也是事務標注方法, 調用本類事務標注方法, 回滾
     */
    @Transactional
    public void runRollbackCase6() throws SQLException {
        runRollbackCase1();
    }

    /*
     * runRollbackCase7 為普通方法, 調用本類事務標注方法, 不回滾
     */
    public void runRollbackCase7() throws SQLException {
        runRollbackCase1();
    }

    /*
     * 提交事務的示例
     */
    @Transactional()
    public void runCommitCase() throws SQLException {
        jdbcTemplate.update("Delete from actor_new where actor_id=?", 12);
    }
}


==========================
參考
==========================
https://www.ibm.com/developerworks/cn/java/j-master-spring-transactional-use/index.html
https://spring.io/guides/gs/managing-transactions/#initial
https://blog.csdn.net/xrt95050/article/details/18076167
https://blog.csdn.net/zq9017197/article/details/6321391?utm_source=blogxgwz0
https://www.cnblogs.com/heartstage/p/3363640.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM