【Spring從入門到精通】03-JdbcTemplate與宣告式事務
目錄
JdbcTemplate與宣告式事務
1、JdbcTemplate
1.1、概述
前面我們已經學習了 Spring 中的Core Container
核心部分和AOP
、Aspects
等面向切面程式設計部分,接下來就是Data Access/Integration
即資料訪問和整合部分
Spring 既可以單獨使用,也可以整合其他框架,如Hibernate
、MyBatis
等。除此之外,其中對於JDBC
也做了封裝,即本章節的JdbcTemplate
,用它可以比較方便地對資料庫進行增刪改查等操作
總結一下:
-
JdbcTemplate
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
層:與資料庫進行互動
因此,我們搭建操作環境也按照典型的三層架構來實現,不過目前現階段我們只關注Service
和Dao
兩層
我們以銀行轉賬為例,因為整個轉賬操作包括兩個操作:出賬的操作和入賬的操作
過程概覽
- 1)建立資料庫表結構,新增幾條記錄
- 2)建立
Service
和Dao
類,完成物件建立和關係注入 - 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)建立Service
和Dao
類,完成物件建立和關係注入
Service
中注入Dao
,Dao
中注入JdbcTemplate
,JdbcTemplate
中注入DataSource
Service
和Dao
類
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
實現類:JdbcTemplate
和MyBatis
框架使用到它 -
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>
對Service
和Dao
類去除註解,並對程式碼稍作修改
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:
PlatformTransactionManager
、DataSourceTrasactionManager
、HibernateTransactionManager
- 宣告式事務兩種實現方式:註解方式和XML方式
- 事務相關引數有:傳播行為、隔離級別、超時時間、是否只讀、(不)回滾
- 傳播行為:有7種傳播屬性,
REQUIRED
、REQUIRED_NEW
、SUPPORTS
、NOT_SOPPORTED
、MANDATORY
、NEVER
、NESTED
- 隔離級別:有3種典型“讀”的問題,髒讀、不可重複讀、虛(幻)讀,可設定4種隔離級別,
READ UNCOMMITTED
、READ COMMITTED
、REPEATABLE READ
、SERIALIZABLE
- 其他引數:
timeout
、readOnly
、rollbackFor
、noRollbackFor
- 傳播行為:有7種傳播屬性,
- 宣告式事務(註解方式):
@Transactional
- 宣告式事務(XML方式):配置事務管理器;配置事務通知
<tx:advice>
;配置切入點和切面 - 完全註解開發:
@EnableTransactionManagement
、@Bean
、AnnotationConfigApplicationContext
總結
-
JdbcTemplate
的CRUD
操作 - 事務
ACID
特性、Spring事務管理 - 宣告式事務的註解方式和XML方式
- 事務相關屬性:傳播行為、隔離級別、其他引數
下面思維導圖經供參考