1. 程式人生 > 其它 >【Spring從入門到精通】03-JdbcTemplate與宣告式事務

【Spring從入門到精通】03-JdbcTemplate與宣告式事務

筆記來源:尚矽谷Spring框架視訊教程(spring5原始碼級講解)

目錄

JdbcTemplate與宣告式事務

1、JdbcTemplate

1.1、概述

前面我們已經學習了 Spring 中的Core Container核心部分和AOPAspects等面向切面程式設計部分,接下來就是Data Access/Integration即資料訪問和整合部分

Spring 既可以單獨使用,也可以整合其他框架,如HibernateMyBatis等。除此之外,其中對於JDBC也做了封裝,即本章節的JdbcTemplate,用它可以比較方便地對資料庫進行增刪改查等操作

總結一下:

  • JdbcTemplate
    就是 Spring 框架對JDBC技術進行的二次封裝模板,能夠簡化對資料庫的操作

1.2、準備工作

步驟預覽

  • 1)引入相關jar
  • 2)Spring 配置檔案配置Druid連線池資訊
  • 3)配置JdbcTemplate物件,注入dataSource
  • 4)建立 Service 和 Dao 類,在 Dao 類中注入JdbcTemplate物件

詳細操作

  • 1)引入相關jar包(或依賴)
    • druid
    • mysql-connector-java
    • spring-jdbc
    • spring-orm
    • spring-tx
  • 2)Spring 配置檔案配置Druid連線池資訊
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${mysql.driverClassName}"/>
    <property name="url" value="${mysql.url}"/>
    <property name="username" value="${mysql.username}"/>
    <property name="password" value="${mysql.password}"/>
</bean>

沿用之前章節的Jdbc.properties配置資訊,但稍作修改

mysql.driverClassName=com.mysql.jdbc.Driver
mysql.url=jdbc:mysql:///book_db
mysql.username=root
mysql.password=root
  • 3)配置JdbcTemplate物件,注入dataSource
<!--配置JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <!--屬性注入dataSource-->
    <property name="dataSource" ref="dataSource"></property>
</bean>

為何使用屬性注入?

JdbcTemplate雖然含有DataSource的有參構造,但其呼叫了setDataSource()方法

這個方法是在其父類中定義了的

  • 4)建立 Service 和 Dao 類,在 Dao 類中注入JdbcTemplate物件

Dao 類

public interface BookDao {
}
@Repository
public class BookDaoImpl implements BookDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
}

Service 類

@Service
public class BookService {
    @Autowired
    private BookDao bookDao;
}

別忘了開啟註解掃描

<!--開啟註解掃描-->
<context:component-scan base-package="com.vectorx.spring5.s15_jdbctemplate"/>

配置檔案整體結構

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!--開啟註解掃描-->
    <context:component-scan base-package="com.vectorx.spring5.s15_jdbctemplate"/>

    <!--配置dataSource-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${mysql.driverClassName}"/>
        <property name="url" value="${mysql.url}"/>
        <property name="username" value="${mysql.username}"/>
        <property name="password" value="${mysql.password}"/>
    </bean>

    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--屬性注入dataSource-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
</beans>

1.3、新增操作

步驟預覽

  • 1)建立資料庫中t_book表對應的實體物件
  • 2)編寫 Service 和 Dao 程式碼,增加新增圖書的功能邏輯
  • 3)程式碼測試

詳細操作

  • 1)建立資料庫中t_book表對應的實體物件

public class Book {
    private String bid;
    private String bname;
    private String bstatus;

    public String getBid() {
        return bid;
    }
    public void setBid(String bid) {
        this.bid = bid;
    }
    public String getBname() {
        return bname;
    }
    public void setBname(String bname) {
        this.bname = bname;
    }
    public String getBstatus() {
        return bstatus;
    }
    public void setBstatus(String bstatus) {
        this.bstatus = bstatus;
    }
}
  • 2)編寫 Service 和 Dao 程式碼,增加新增圖書的功能邏輯

Service 類:新增addBook()方法

@Service
public class BookService {
    @Autowired
    private BookDao bookDao;

    public int addBook(Book book) {
        return bookDao.add(book);
    }
}

Dao 類:通過操作JdbcTemplate物件的update()方法可實現插入,其中兩個引數分別是

  • 第一個引數sql:編寫插入資料對應的sql語句,可使用萬用字元?做佔位符
  • 第二個引數args:可變引數列表,設定佔位符對應的引數值
public interface BookDao {
    int add(Book book);
}
@Repository
public class BookDaoImpl implements BookDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
 
    @Override
    public int add(Book book) {
        //操作JdbcTemplate物件,使用update方法進行新增操作
        String sql = "insert into t_book(bid,bname,bstatus) values(?,?,?)";
        Object[] args = {book.getBid(), book.getBname(), book.getBstatus()};
        return jdbcTemplate.update(sql, args);
    }
}
  • 3)程式碼測試
ApplicationContext context = new ClassPathXmlApplicationContext("bean13.xml");
BookService bookService = context.getBean("bookService", BookService.class);

//模擬新增圖書
Book book = new Book();
book.setBid("1");
book.setBname("Spring JdbcTemplate");
book.setBstatus("1");
int result = bookService.addBook(book);
System.out.println(result);

測試結果

Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
三月 06, 2022 10:25:49 下午 com.alibaba.druid.pool.DruidDataSource info
資訊: {dataSource-1} inited
1

重新整理資料庫中t_book表資料,核驗是否插入成功

可以看到,表中成功新增了一條資料

1.4、修改和刪除

修改、刪除操作和新增操作程式碼邏輯基本一致

BookService 類:新增updateBook()deleteBook()方法

// 修改
public int updateBook(Book book) {
    return bookDao.update(book);
}
//刪除
public int deleteBook(String id) {
    return bookDao.delete(id);
}

BookDao 類:新增update()delete()方法

// 修改
int update(Book book);
// 刪除
int delete(String id);

BookDaoImpl 類:實現update()delete()方法

// 修改
@Override
public int update(Book book) {
    String sql = "update t_book set bname=?,bstatus=? where bid=?";
    Object[] args = {book.getBname(), book.getBstatus(), book.getBid()};
    return jdbcTemplate.update(sql, args);
}
// 刪除
@Override
public int delete(String id) {
    String sql = "delete from t_book where bid=? ";
    return jdbcTemplate.update(sql, id);
}

測試修改

//修改圖書資訊
Book book = new Book();
book.setBid("1");
book.setBname("JdbcTemplate");
book.setBstatus("update");
int result2 = bookService.updateBook(book);
System.out.println(result2);

測試結果

測試刪除

//刪除圖書
int result3 = bookService.deleteBook("1");
System.out.println(result3);

測試結果

1.5、查詢操作

這裡演示三種查詢操作:

  • 1)查詢返回某個值
  • 2)查詢返回物件
  • 3)查詢返回集合

為了演示效果,需要先在資料庫的t_book表中新增兩條資料

接著我們先將程式碼完成,最後再作進一步的分析說明

程式碼實現

BookService 類:新增findCount()findById()findAll()方法

// 查詢返回一個值
public int findCount() {
    return bookDao.selectCount();
}
// 查詢返回物件
public Book findById(String id) {
    return bookDao.selectById(id);
}
// 查詢返回集合
public List<Book> findAll() {
    return bookDao.selectAll();
}

BookDao 類:新增selectCount()selectById()selectAll()方法

// 查詢返回一個值
int selectCount();
// 查詢返回物件
Book selectById(String id);
// 查詢返回集合
List<Book> selectAll();

BookDaoImpl 類:實現selectCount()selectById()selectAll()方法

// 查詢返回一個值
@Override
public int selectCount() {
    String sql = "select count(0) from t_book";
    return jdbcTemplate.queryForObject(sql, Integer.class);
}
// 查詢返回物件
@Override
public Book selectById(String id) {
    String sql = "select * from t_book where bid=?";
    return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);
}
// 查詢返回集合
@Override
public List<Book> selectAll() {
    String sql = "select * from t_book where 1=1";
    return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Book.class));
}

測試程式碼

int count = bookService.findCount();
System.out.println(count);
Book book = bookService.findById("1");
System.out.println(book);
List<Book> bookList = bookService.findAll();
System.out.println(bookList);

測試結果

2
Book{bid='1', bname='Spring', bstatus='add'}
[Book{bid='1', bname='Spring', bstatus='add'}, Book{bid='2', bname='SpringMVC', bstatus='add'}]

程式碼分析

上述程式碼邏輯中使用到了queryForObject()query()方法

jdbcTemplate.queryForObject(sql, Integer.class);
jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);
jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Book.class));

分別對應JdbcTemplate中的三個方法

public <T> T queryForObject(String sql, Class<T> requiredType);
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args);
public <T> List<T> query(String sql, RowMapper<T> rowMapper);

其中,有兩個引數值得關注,一個是Class<T> requiredType,另一個是RowMapper<T> rowMapper

  • Class<T> requiredType:返回值的Class型別
  • RowMapper<T> rowMapper:是一個介面,返回不同型別資料,可以使用其實現類進行資料的封裝。其實現類有很多,因為我們需要返回一個數據庫實體物件,所以可以選擇使用BeanPropertyRowMapper

另外,queryForObject(String sql, RowMapper<T> rowMapper, Object... args)query(String sql, RowMapper<T> rowMapper)

區別在於

  • queryForObject返回一個物件
  • query返回一個集合

1.6、批量操作

JdbcTemplate中提供了batchUpdate()可供我們進行批量操作,如:批量新增、批量修改、批量刪除等,程式碼實現上大同小異,我們對程式碼進行快速實現

程式碼實現

BookService 類:新增batchAddBook()batchUpdateBook()batchDelBook()方法

// 批量新增
public void batchAddBook(List<Object[]> bookList) {
    bookDao.batchAdd(bookList);
}
// 批量修改
public void batchUpdateBook(List<Object[]> bookList) {
    bookDao.batchUpdate(bookList);
}
// 批量刪除
public void batchDelBook(List<Object[]> bookList) {
    bookDao.batchDel(bookList);
}

BookDao 類:新增batchAdd()batchUpdate()batchDel()方法

// 批量新增
void batchAdd(List<Object[]> bookList);
// 批量修改
void batchUpdate(List<Object[]> bookList);
// 批量刪除
void batchDel(List<Object[]> bookList);

BookDaoImpl 類:實現batchAdd()batchUpdate()batchDel()方法

// 批量新增
@Override
public void batchAdd(List<Object[]> bookList) {
    String sql = "insert into t_book(bid,bname,bstatus) values(?,?,?)";
    extractBatch(sql, bookList);
}
// 批量修改
@Override
public void batchUpdate(List<Object[]> bookList) {
    String sql = "update t_book set bname=?,bstatus=? where bid=?";
    extractBatch(sql, bookList);
}
// 批量刪除
@Override
public void batchDel(List<Object[]> bookList) {
    String sql = "delete from t_book where bid=? ";
    extractBatch(sql, bookList);
}
private void extractBatch(String sql, List<Object[]> bookList,) {
    int[] ints = jdbcTemplate.batchUpdate(sql, bookList);
    System.out.println(ints);
}

程式碼測試

測試批量新增

// 批量新增
List<Object[]> bookList = new ArrayList<>();
Object[] book1 = {"3", "Java", "batchAdd"};
Object[] book2 = {"4", "Python", "batchAdd"};
Object[] book3 = {"5", "C#", "batchAdd"};
bookList.add(book1);
bookList.add(book2);
bookList.add(book3);
bookService.batchAddBook(bookList);	

測試結果

測試批量修改

// 批量修改
List<Object[]> bookList = new ArrayList<>();
Object[] book1 = {"Java++", "batchUpdate", "3"};
Object[] book2 = {"Python++", "batchUpdate", "4"};
Object[] book3 = {"C#++", "batchUpdate", "5"};
bookList.add(book1);
bookList.add(book2);
bookList.add(book3);
bookService.batchUpdateBook(bookList);

測試結果

測試批量刪除

// 批量刪除
List<Object[]> bookList = new ArrayList<>();
Object[] book1 = {"3"};
Object[] book2 = {"4"};
bookList.add(book1);
bookList.add(book2);
bookService.batchDelBook(bookList);

測試結果

可以看出,上述測試都完全符合我們的預期

小結

簡單總結下JdbcTemplate操作資料庫的各個方法

  • 新增、修改、刪除操作:update()方法
  • 查詢操作:queryForObject()query()方法,關注兩個引數:
    • Class<T> requiredType:返回值的Class型別
    • RowMapper<T> rowMapper:介面,具體實現類BeanPropertyRowMapper,封裝物件實體
  • 批量操作:batchUpdate()方法

2、事務

2.1、事務概念

  • 1)事務是資料庫操作的最基本單元,是邏輯上的一組操作。這一組操作,要麼都成功,要麼都失敗(只要有一個操作失敗,所有操作都失敗)
  • 2)典型場景:銀行轉賬。Lucy 轉賬 100 元給 Mary,Lucy 少 100,Mary 多 100。轉賬過程中若出現任何問題,雙方都不會多錢或少錢,轉賬就不會成功

2.2、事務四個特性(ACID)

  • 原子性(Atomicity):一個事務中的所有操作,要麼都成功,要麼都失敗,整個過程不可分割
  • 一致性(Consistency):事務操作之前和操作之後,總量保持不變
  • 隔離性(Isolation):多事務操作時,相互之間不會產生影響
  • 永續性(Durability):事務最終提交後,資料庫表中資料才會真正發生變化

2.3、搭建事務操作環境

我們知道 JavaEE 中的三層架構分為:表示層(web層)、業務邏輯層(service層)、資料訪問層(dao層)

  • web層:與客戶端進行互動
  • service層:處理業務邏輯
  • dao層:與資料庫進行互動

因此,我們搭建操作環境也按照典型的三層架構來實現,不過目前現階段我們只關注ServiceDao兩層

我們以銀行轉賬為例,因為整個轉賬操作包括兩個操作:出賬的操作和入賬的操作

過程概覽

  • 1)建立資料庫表結構,新增幾條記錄
  • 2)建立ServiceDao類,完成物件建立和關係注入
  • 3)Dao中建立兩個方法:出賬的方法、入賬的方法;Service中建立轉賬的方法

過程詳解

1)建立資料庫表結構,新增幾條記錄

# 建表語句
create table t_account
(
    id       varchar(20) not null,
    username varchar(50) null,
    amount   int         null,
    constraint transfer_record_pk
        primary key (id)
);
# 新增語句
INSERT INTO book_db.t_account (id, username, amount) VALUES ('1', 'Lucy', 1000);
INSERT INTO book_db.t_account (id, username, amount) VALUES ('2', 'Mary', 1000);

新增完成效果

2)建立ServiceDao類,完成物件建立和關係注入

Service中注入DaoDao中注入JdbcTemplateJdbcTemplate中注入DataSource

ServiceDao

public interface TransferRecordDao {
}
@Repository
public class TransferRecordDaoImpl implements TransferRecordDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
}
@Service
public class TransferRecordService {
    @Autowired
    private TransferRecordDao transferRecordDao;
}

Spring 配置檔案

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!--開啟註解掃描-->
    <context:component-scan base-package="com.vectorx.spring5.s16_transaction"/>
    <!--配置dataSource-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${mysql.driverClassName}"/>
        <property name="url" value="${mysql.url}"/>
        <property name="username" value="${mysql.username}"/>
        <property name="password" value="${mysql.password}"/>
    </bean>
    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--屬性注入dataSource-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
</beans>

3)Dao中建立兩個方法:出賬的方法、入賬的方法;Service中建立轉賬的方法

  • Dao負責資料庫操作,所以需要建立兩個方法:出賬的方法、入賬的方法
public interface TransferRecordDao {
    void transferOut(int amount, String username);
    void transferIn(int amount, String username);
}
@Repository
public class TransferRecordDaoImpl implements TransferRecordDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void transferOut(int amount, String username) {
        String sql = "update t_account set amount=amount-? where username=?";
        Object[] args = {amount, username};
        jdbcTemplate.update(sql, args);
    }
    @Override
    public void transferIn(int amount, String username) {
        String sql = "update t_account set amount=amount+? where username=?";
        Object[] args = {amount, username};
        jdbcTemplate.update(sql, args);
    }
}
  • Service負責業務操作,所以需要建立一個方法,來呼叫Dao中兩個方法
@Service
public class TransferRecordService {
    @Autowired
    private TransferRecordDao transferRecordDao;

    public void transferAccounts(int amount, String fromUser, String toUser) {
        transferRecordDao.transferOut(amount, fromUser);
        transferRecordDao.transferIn(amount, toUser);
    }
}

測試程式碼

ApplicationContext context = new ClassPathXmlApplicationContext("bean14.xml");
TransferRecordService transferRecordService = context.getBean("transferRecordService", TransferRecordService.class);
transferRecordService.transferAccounts(100, "Lucy", "Mary");

測試結果

可以發現,轉賬如期完成了。但真的沒有一點問題麼?

2.4、引入事務場景

我們模擬下在轉賬中途發生網路異常,修改TransferRecordService中轉賬方法

public void transferAccounts(int amount, String fromUser, String toUser) {
    transferRecordDao.transferOut(amount, fromUser);
    //模擬網路異常而導致操作中斷
    int i = 10 / 0;
    transferRecordDao.transferIn(amount, toUser);
}

為了更清晰直觀地看到資料的變化,我們還原資料表資料到最初狀態

按照期望,轉賬應該失敗,即雙方賬戶不應該有任何變化。事實真的能夠如我們所料麼?

我們執行測試方法,如期丟擲異常

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at com.vectorx.spring5.s16_transaction.service.TransferRecordService.transferAccounts(TransferRecordService.java:15)
    at com.vectorx.spring5.s16_transaction.TestTransfer.main(TestTransfer.java:11)

那資料表是否也如期變化呢?

我們發現,Lucy雖然成功轉出了 100 元,但Mary沒有成功到賬 100 元。從現實的角度來說,這個問題很嚴重!!!

從事務的角度來說,這個轉賬操作沒有遵循事務的原子性、一致性,即沒有做到“要麼都成功,要麼都失敗”,也沒有做到“操作前後的總量不變”

綜上所述,我們需要引入事務

2.5、事務基本操作

事務的基本操作過程如下

  • Step1、開啟一個事務
  • Step2、進行業務邏輯實現
  • Step3、沒有異常,則提交事務
  • Step4、發生異常,則回滾事務

事務的一般實現如下

try {
    // Step1、開啟一個事務
    // Step2、進行業務邏輯實現
    transferRecordDao.transferOut(amount, fromUser);
    //模擬網路異常而導致操作中斷
    int i = 10 / 0;
    transferRecordDao.transferIn(amount, toUser);
    // Step3、沒有異常,則提交事務
} catch (Exception e) {
    // Step4、發生異常,則回滾事務
}

不過,在 Spring 框架中提供了更方便的方式實現事務。“欲知後事如何,且聽下回分解”

小結

本小結主要內容關鍵點

  • 事務的基本概念:資料庫操作的基本單元,邏輯上的一組操作,要麼都成功,要麼都失敗
  • 事務的四個基本特性:ACID,即原子性、一致性、隔離性和永續性

3、宣告式事務

3.1、Spring事務管理

事務一般新增到三層結構中的Service層(業務邏輯層)

在 Spring 中進行事務管理操作有兩種方式:程式設計式事務管理宣告式事務管理

  • 程式設計式事務管理(不推薦):上述事務的一般實現就是典型的程式設計式事務管理實現。但這種方式雖然並不好,但仍然需要我們有一定的瞭解,知道有這麼一個過程即可。一般不推薦使用這種方式,主要原因如下
    • 1)實現不方便
    • 2)造成程式碼臃腫
    • 3)維護起來麻煩
  • 宣告式事務管理(推薦使用):底層原理就是AOP,就是在不改變原始碼基礎上,擴充套件程式碼功能。有兩種實現方式
    • 基於註解方式(推薦方式)
    • 基於XML配置檔案方式

3.2、Spring事務管理API

提供了一個介面,代表事務管理器,針對不同框架存在不同實現類

主要掌握

  • PlatformTransactionManager介面:即事務管理器,有多個不同的抽象類和具體實現類,可滿足不同的框架
  • DataSourceTrasactionManager實現類:JdbcTemplateMyBatis框架使用到它
  • HibernateTransactionManager實現類:Hibernate框架使用到它

3.3、宣告式事務(註解方式)

  • 1)在 Spring 配置檔案中配置事務管理器:配置DataSourceTransactionManager物件建立
<!--配置事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>
  • 2)在 Spring 配置檔案中開啟事務:引入xmlns:tx的名稱空間,並配置<tx:annotation-driven>標籤以開啟事務註解
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--其他配置資訊略-->
    <!--開啟事務註解-->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>
  • 3)在Service(或Service的方法)上新增事務註解@Transactional
@Service
@Transactional
public class TransferRecordService {
    //...
}

首先還原資料表資訊至初始狀態

測試程式碼後重新整理資料表

這一次資料沒有發生變化,說明事務回滾了,符合我們預期

3.4、事務引數

Service類上面添加註解@Transactional,在這個註解裡面可以配置事務相關引數

主要介紹引數有

  • propagation:事務傳播行為
  • isolation:事務隔離級別
  • timeout:超時時間
  • readOnly:是否只讀
  • rollbackFor:回滾
  • noRollbackFor:不回滾

3.5、傳播行為

  • 事務傳播行為:多事務方法直接進行呼叫,這個過程中事務是如何進行管理的
  • 事務方法:讓資料表資料發生變化的操作

事務的傳播行為可以有傳播屬性指定。Spring 框架中定義了 7 種類傳播行為:

傳播屬性 描述
REQUIRED 如果有事務在執行,當前方法就在此事務內執行;否則,就啟動一個新的事務,並在自己的事務內執行
REQUIRED_NEW 當前方法必須啟動新事務,並在它自己的事務內執行;如果有事務正在執行,應該將它掛起
SUPPORTS 如果有事務在執行,當前方法就在此事務內執行;否則,它可以不執行在事務中
NOT_SOPPORTED 當前方法不應該執行在事務內部,如果有執行的事務,就將它掛起
MANDATORY 當前方法必須執行在事務內部,如果沒有正在執行的事務,就丟擲異常
NEVER 當前方法不應該執行在事務中,如果有執行的事務,就丟擲異常
NESTED 如果有事務在執行,當前方法就應該在此事務的巢狀事務內執行;否則,就啟動一個新的事務,並在它自己的事務內執行

舉個例子:定義兩個方法add()update()

@Transactional
public void add(){
    // 呼叫update方法
    update();
}
public void update(){
    // do something
}

則按照不同的傳播屬性,可以有以下解釋

  • REQUIRED
    • 如果add()方法本身有事務,呼叫update()方法之後,update()使用當前add()方法裡面事務;
    • 如果add()方法本身沒有事務,呼叫update()方法之後,建立新的事務
  • REQUIRED_NEW
    • 使用add()方法呼叫update()方法,無論add()是否有事務,都建立新的事務

程式碼實現

@Service
@Transactional(propagation = Propagation.REQUIRED)
public class TransferRecordService {
    //...
}

等價於

@Service
@Transactional
public class TransferRecordService {
    //...
}

即預設不寫,其事務傳播行為就是使用的REQUIRED

3.6、隔離級別

在事務的四個特性中,隔離性(isolation)指的是多事務之間互不影響。不考慮隔離性,在併發時會產生一系列問題

有三個典型的“讀”的問題:髒讀、不可重複讀、虛(幻)讀

  • 髒讀:一個未提交事務讀取到了另一個未提交事務修改的資料
  • 不可重複讀:一個未提交事務讀取到了另一個已提交事務修改的資料(不能算問題,只是算現象)
  • 虛(幻)讀:一個未提交事務讀取到了另一個已提交事務新增的資料

通過設定隔離級別,可以解決上述“讀”的問題

事務隔離級別 髒讀 不可重複讀 幻讀
READ UNCOMMITTED(讀未提交)
READ COMMITTED(讀已提交) ×
REPEATABLE READ(可重複讀) × ×
SERIALIZABLE(序列化) × × ×

程式碼實現

@Service
@Transactional(isolation = Isolation.REPEATABLE_READ)
public class TransferRecordService {
    //...
}

小課堂:MySQL 中預設事務隔離級別為REPEATABLE READ(可重複讀)

3.7、其他引數

  • timeout:設定事務超時時間。事務需要在一定的時間內進行提交,若設定時間內事務未完成提交,則對事務進行回滾。預設值為-1,設定時間以秒為單位
@Service
@Transactional(timeout = 5)
public class TransferRecordService {
    @Autowired
    private TransferRecordDao transferRecordDao;

    public void transferAccounts(int amount, String fromUser, String toUser) {
        transferRecordDao.transferOut(amount, fromUser);
        //模擬處理超時
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        transferRecordDao.transferIn(amount, toUser);
    }
}

設定超時時間後,執行測試程式碼,則日誌資訊會丟擲TransactionTimedOutException事務超時異常

Exception in thread "main" org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Wed Mar 09 21:30:33 CST 2022
    at org.springframework.transaction.support.ResourceHolderSupport.checkTransactionTimeout(ResourceHolderSupport.java:155)
    at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInMillis(ResourceHolderSupport.java:144)
    at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInSeconds(ResourceHolderSupport.java:128)
    at org.springframework.jdbc.datasource.DataSourceUtils.applyTimeout(DataSourceUtils.java:341)
...	
  • readOnly:是否只讀。
    • 預設值為false,表示讀寫操作都允許,可以進行增、刪、改、查等操作;
    • 可設定為true,表示只允許讀操作,即只能進行查詢操作
@Service
@Transactional(readOnly = true)
public class TransferRecordService {
    //...
}

設定只讀後,執行測試程式碼,則日誌資訊會丟擲TransientDataAccessResourceException瞬態資料訪問資源異常,同時還會丟擲SQLException,並指出“連線是隻讀的,查詢導致資料修改是不允許的”

Exception in thread "main" org.springframework.dao.TransientDataAccessResourceException: PreparedStatementCallback; SQL [update t_account set amount=amount-? where username=?]; Connection is read-only. Queries leading to data modification are not allowed; nested exception is java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
	...
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
    ...
  • rollbackFor:設定出現哪些異常才進行回滾
@Service
@Transactional(rollbackFor = ArithmeticException.class)
public class TransferRecordService {
    @Autowired
    private TransferRecordDao transferRecordDao;

    public void transferAccounts(int amount, String fromUser, String toUser) {
        transferRecordDao.transferOut(amount, fromUser);
        //模擬網路異常而導致操作中斷
        int i = 10 / 0;
        transferRecordDao.transferIn(amount, toUser);
    }
}

上述程式碼表明,只有在丟擲的異常為ArithmeticException時,才會進行事務的回滾操作

此時執行測試程式碼,後臺會丟擲ArithmeticException異常,因此會進行回滾,轉賬過程不會成功

此時資料庫中的資料,就不會有任何變化

  • noRollbackFor:設定出現哪些異常不進行回滾
@Service
@Transactional(noRollbackFor = ArithmeticException.class)
public class TransferRecordService {
    @Autowired
    private TransferRecordDao transferRecordDao;

    public void transferAccounts(int amount, String fromUser, String toUser) {
        transferRecordDao.transferOut(amount, fromUser);
        //模擬網路異常而導致操作中斷
        int i = 10 / 0;
        transferRecordDao.transferIn(amount, toUser);
    }
}

因為設定了noRollbackFor = ArithmeticException.class,即表示丟擲ArithmeticException異常時不會進行回滾

此時執行測試程式碼,後臺會丟擲ArithmeticException異常,但不會進行回滾,轉賬事務中只有出賬操作會成功

Exception in thread "main" java.lang.ArithmeticException: / by zero

此時資料庫中的資料,就會是下面情況(顯然,這並不是我們想要的)

3.8、宣告式事務(XML方式)

  • Step1、配置事務管理器
<!--配置事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>
  • Step2、配置事務通知
<!--1、配置事務通知-->
<tx:advice id="txAdvice">
    <tx:attributes>
        <tx:method name="transferAccounts" propagation="REQUIRED" isolation="REPEATABLE_READ" read-only="false"
                   timeout="5" rollback-for="java.lang.ArithmeticException"/>
    </tx:attributes>
</tx:advice>
  • Step3、配置切入點和切面
<!--2、配置切入點和切面-->
<aop:config>
    <aop:pointcut id="p"
                  expression="execution(* com.vectorx.spring5.s17_transaction_xml.service.TransferRecordService.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="p"></aop:advisor>
</aop:config>

Spring 配置檔案整體內容

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置dataSource-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${mysql.driverClassName}"/>
        <property name="url" value="${mysql.url}"/>
        <property name="username" value="${mysql.username}"/>
        <property name="password" value="${mysql.password}"/>
    </bean>

    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--屬性注入dataSource-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置事務管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置Dao建立和屬性注入-->
    <bean id="transferRecordDao" class="com.vectorx.spring5.s17_transaction_xml.dao.TransferRecordDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"></property>
    </bean>

    <!--配置Service建立和屬性注入-->
    <bean id="transferRecordService" class="com.vectorx.spring5.s17_transaction_xml.service.TransferRecordService">
        <property name="transferRecordDao" ref="transferRecordDao"></property>
    </bean>

    <!--1、配置事務通知-->
    <tx:advice id="txAdvice">
        <tx:attributes>
            <tx:method name="transferAccounts" propagation="REQUIRED" isolation="REPEATABLE_READ" read-only="false"
                       timeout="5" rollback-for="java.lang.ArithmeticException"/>
        </tx:attributes>
    </tx:advice>

    <!--2、配置切入點和切面-->
    <aop:config>
        <aop:pointcut id="p"
                      expression="execution(* com.vectorx.spring5.s17_transaction_xml.service.TransferRecordService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="p"></aop:advisor>
    </aop:config>
</beans>

ServiceDao類去除註解,並對程式碼稍作修改

public class TransferRecordDaoImpl implements TransferRecordDao {
    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    //...
}
public class TransferRecordService {
    private TransferRecordDao transferRecordDao;

    public void setTransferRecordDao(TransferRecordDao transferRecordDao) {
        this.transferRecordDao = transferRecordDao;
    }
    //...
}

執行測試程式碼

後臺結果

Exception in thread "main" java.lang.ArithmeticException: / by zero

資料庫結果

3.9、完全註解開發

// 表示此類為配置類
@Configuration
// 自動掃描包
@ComponentScan(basePackages = "com.vectorx.spring5.s18_transaction_annotation")
// 開啟事務
@EnableTransactionManagement
// 讀取外部配置檔案
@PropertySource(value = {"classpath:jdbc.properties"})
public class TxConfig {
    @Value(value = "${mysql.driverClassName}")
    private String driverClassName;
    @Value(value = "${mysql.url}")
    private String url;
    @Value(value = "${mysql.username}")
    private String username;
    @Value(value = "${mysql.password}")
    private String password;

    //配置dataSource
    @Bean
    public DruidDataSource getDruidDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    //配置JdbcTemplate
    @Bean
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        //IOC容器會根據型別找到對應DataSource
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    //配置事務管理器
    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}

這裡我們對各個註解進行一一說明

  • @Configuration:表示此類為一個配置類,其作用等同於建立一個bean.xml,即
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
  • @ComponentScan:自動掃描包,basePackages屬性配置為需要掃描的包路徑,等價於<context:component-scan>標籤,即
<!--開啟註解掃描-->
<context:component-scan base-package="com.vectorx.spring5.s18_transaction_annotation"/>
  • @EnableTransactionManagement:開啟事務管理,等價於<tx:annotation-driven>標籤,即
<!--開啟事務註解-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
  • @PropertySource:引入外部檔案,value配置外部檔案路徑,等價於<context:property-placeholder>標籤
<context:property-placeholder location="classpath:jdbc.properties"/>
  • @Value:對普通型別的屬性進行注入,同時其屬性值可以使用${}表示式對外部檔案配置資訊進行獲取
  • @Bean:配置物件建立,等價於<bean>標籤。可以在被修飾的方法引數列表中傳入受IOC容器管理的型別,IOC容器會自動根據型別找到對應物件並注入到此方法中。因此無論是配置JdbcTemplate還是配置事務管理器,都可以使用這種方式對外部Bean進行引用
//配置JdbcTemplate
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource) {...}
//配置事務管理器
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {...}

測試程式碼

需要注意的是,之前建立的物件是ClassPathXmlApplicationContext,而現在是完全註解開發,所以需要建立的物件是AnnotationConfigApplicationContext,構造引數中傳入配置類的class型別即可,其他程式碼與之前一致

ApplicationContext context = new AnnotationConfigApplicationContext(TxConfig.class);
TransferRecordService transferRecordService = context.getBean("transferRecordService", TransferRecordService.class);
transferRecordService.transferAccounts(100, "Lucy", "Mary");

測試結果

小結

重點掌握

  • Spring事務管理兩種方式:程式設計式事務管理(不推薦)、宣告式事務管理(推薦)
  • Spring事務管理API:PlatformTransactionManagerDataSourceTrasactionManagerHibernateTransactionManager
  • 宣告式事務兩種實現方式:註解方式XML方式
  • 事務相關引數有:傳播行為、隔離級別、超時時間、是否只讀、(不)回滾
    • 傳播行為:有7種傳播屬性,REQUIREDREQUIRED_NEWSUPPORTSNOT_SOPPORTEDMANDATORYNEVERNESTED
    • 隔離級別:有3種典型“讀”的問題,髒讀、不可重複讀、虛(幻)讀,可設定4種隔離級別,READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
    • 其他引數:timeoutreadOnlyrollbackFornoRollbackFor
  • 宣告式事務(註解方式):@Transactional
  • 宣告式事務(XML方式):配置事務管理器;配置事務通知<tx:advice>;配置切入點和切面
  • 完全註解開發:@EnableTransactionManagement@BeanAnnotationConfigApplicationContext

總結

  1. JdbcTemplateCRUD操作
  2. 事務ACID特性、Spring事務管理
  3. 宣告式事務的註解方式和XML方式
  4. 事務相關屬性:傳播行為、隔離級別、其他引數

下面思維導圖經供參考