1、JdbcTemplate
爲了使 JDBC 更加易於使用,Spring 在 JDBC API 上定義了一個抽象層,以此建立一個 JDBC 存取框架。
作爲 Spring JDBC 框架的核心,JDBC 模板的設計目的是爲不同類型的 JDBC 操作提供模板方法。每個模板方法都能控制整個過程,並允許覆蓋過程中的特定任務。 通過這種方式, 可以在儘可能保留靈活性的情況下,將數據庫存取的工作量降到最低。
1.1 MySql數據庫環境準備:
CREATE DATABASE spring4;
USE spring4;
CREATE TABLE t_employee (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(50),
email VARCHAR(50),
dept_id INT
);
CREATE TABLE t_dept(
id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(100)
);
ALTER TABLE t_employee ADD CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES t_dept(id);
INSERT INTO t_dept VALUES (1,'研發部');
INSERT INTO t_dept VALUES (2,'技術部');
INSERT INTO t_employee VALUES (1,'tom','[email protected]',1);
INSERT INTO t_employee VALUES (2,'bob','[email protected]',2);
INSERT INTO t_employee VALUES (3,'jack','[email protected]',1);
INSERT INTO t_employee VALUES (4,'jerry','[email protected]',2);
ALTER TABLE t_employee CHANGE COLUMN NAME last_name VARCHAR(50);
1.2 創建普通Java工程
1.2.1 創建工程,導入jar包
跟之前一樣創建Java工程,需要導入的jar包:
1.2.2 配置文件
在類路徑下創建conf資源文件夾,創建db.properties文件
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///spring4
jdbc.username=root
jdbc.password=123
jdbc.initPoolSize=5
jdbc.maxPoolSize=20
conf下面再創建spring的配置文件目錄,裏面創建applicationContext.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"
xmlns:aop="http://www.springframework.org/schema/aop"
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/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--引入外部數據源-->
<context:property-placeholder location="classpath:db.properties"/>
<!--配置數據源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"/>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
</bean>
<!--配置Spring的JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--他必須要配置一個數據源-->
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
1.2.3 創建表對應的實體
package com.spring.jdbc;
public class Employee {
private Integer id;
private String lastName;
private String email;
private Department department;
//get/set以及toString方法
}
package com.spring.jdbc;
public class Department {
private Integer id;
private String departmentName;
//get/set以及toString方法
}
1.2.4 JdbcTemplate的測試
package com.spring.jdbc;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class JdbcTemplateTest {
private ApplicationContext applicationContext;
private JdbcTemplate jdbcTemplate;
@Before//在執行其他測試方法之前,會先執行該方法
public void init(){
applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
jdbcTemplate = applicationContext.getBean(JdbcTemplate.class);
}
/**
* 獲取單個列的值, 或做統計查詢
* 使用 queryForObject(String sql, Class<Long> requiredType)
*/
@Test
public void testQueryForObject2(){
String sql = "select count(*) from t_employee";
Long count = jdbcTemplate.queryForObject(sql, Long.class);
System.out.println(count);
}
/**
* 查到實體類的集合
* 注意調用的不是 queryForList 方法
*/
@Test
public void testQueryForList(){
String sql = "SELECT id, last_name lastName, email FROM t_employee WHERE id < ?";
RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
List<Employee> employees = jdbcTemplate.query(sql, rowMapper,5);
System.out.println(employees);
}
/**
* 從數據庫中獲取一條記錄, 實際得到對應的一個對象
* 注意不是調用 queryForObject(String sql, Class<Employee> requiredType, Object... args) 方法!
* 而需要調用 queryForObject(String sql, RowMapper<Employee> rowMapper, Object... args)
* 1. 其中的 RowMapper 指定如何去映射結果集的行, 常用的實現類爲 BeanPropertyRowMapper
* 2. 使用 SQL 中列的別名完成列名和類的屬性名的映射. 例如 last_name lastName
* 3. 不支持級聯屬性. JdbcTemplate 到底是一個 JDBC 的小工具, 而不是 ORM 框架
*/
@Test
public void testQueryForObject(){
String sql = "select id,last_name lastName,email,dept_id deptId from t_employee where id = ?";
RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
Employee employee = jdbcTemplate.queryForObject(sql, rowMapper, 1);
System.out.println(employee);
}
/**
* 執行批量更新: 批量的 INSERT, UPDATE, DELETE
* 最後一個參數是 Object[] 的 List 類型: 因爲修改一條記錄需要一個 Object 的數組, 那麼多條不就需要多個 Object 的數組嗎
*/
@Test
public void testBatchUpdate(){
String sql = "insert into t_employee (last_name,email,dept_id) values (?,?,?)";
List<Object[]> list = new ArrayList<>();
Object[] objects1 = new Object[]{"A","[email protected]",1};
Object[] objects2 = new Object[]{"B","[email protected]",2};
Object[] objects3 = new Object[]{"C","[email protected]",2};
Object[] objects4 = new Object[]{"D","[email protected]",1};
list.add(objects1);
list.add(objects2);
list.add(objects3);
list.add(objects4);
//批量新增
int[] inserts = jdbcTemplate.batchUpdate(sql, list);
System.out.println(inserts.length);
//刪除語句
sql = "delete from t_employee where name = ?";
List<Object[]> nameList = new ArrayList<>();
nameList.add(new Object[]{"A"});
nameList.add(new Object[]{"B"});
nameList.add(new Object[]{"C"});
nameList.add(new Object[]{"D"});
//批量刪除
int[] deletes = jdbcTemplate.batchUpdate(sql, nameList);
System.out.println(deletes.length);
}
// update 方法可以進行 INSERT UPDATE DELETE操作
@Test
public void testUpdate(){
//單條新增
String sql = "insert into t_employee (last_name,email,dept_id) values (?,?,?)";
Object[] args = new Object[]{"pipi","[email protected]",2};
int insert = jdbcTemplate.update(sql, args);
System.out.println(insert);
//單條修改
String sql2 = "update t_employee set last_name = ? where id = ?";
int update = jdbcTemplate.update(sql2, "bobo", 5);
System.out.println(update);
//單條刪除
String sql3 = "delete from t_employee where id = ?";
int delete = jdbcTemplate.update(sql3, 5);
System.out.println(delete);
}
//測試是否可以獲取到連接
@Test
public void testConnection() throws SQLException {
DataSource da = applicationContext.getBean(DataSource.class);
System.out.println(da.getConnection());
}
}
1.3 實際項目中JdbcTemplate的用法
Spring JDBC 框架還提供了一個 JdbcDaoSupport 類來簡化 DAO 實現,該類聲明瞭 jdbcTemplate 屬性, 它可以從 IOC 容器中注入,或者自動從數據源中創建。(不推薦使用,使用起來沒那麼方便,需要注入數據源,但是不能通過註解注入)。
實際開發中,JdbcTemplate會在持久層(Dao)進行注入,然後配置文件配置掃描,掃描持久層類上的@Repository註解,在其他業務層,注入持久層就行了:
1.3.1 修改配置文件
配置文件中,添加包掃描:
<!--配置包掃描,掃描@Repository @Service @Controller-->
<context:component-scan base-package="com.spring.jdbc"/>
1.3.2 創建Dao持久層
package com.spring.jdbc;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
@Repository
public class EmployeeDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public Employee getById(Integer id){
String sql = "select id , last_name lastName,email from t_employee where id = ?";
RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
Employee employee = jdbcTemplate.queryForObject(sql, rowMapper, 1);
return employee;
}
}
1.3.3 測試
public class JdbcTemplateTest {
private ApplicationContext applicationContext;
private JdbcTemplate jdbcTemplate;
private EmployeeDao employeeDao;
@Before//在執行其他測試方法之前,會先執行該方法
public void init(){
applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
jdbcTemplate = applicationContext.getBean(JdbcTemplate.class);
employeeDao = applicationContext.getBean(EmployeeDao.class);
}
/**
* 一般項目中,會有一個持久層,持久層中注入JdbcTemplate,然後業務層再調用持久層的方法
*/
@Test
public void getByDao(){
Employee en = employeeDao.getById(1);
System.out.println(en);
}
}
2、NamedParameterJdbcTemplate
2.1 配置NamedParameterJdbcTemplate
<!--配置NamedParameterJdbcTemplate 該對象可以使用具名參數, 其沒有無參數的構造器, 所以必須爲其構造器指定參數-->
<bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg name="dataSource" ref="dataSource"/>
</bean>
2.2 測試
package com.spring.jdbc;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import java.util.HashMap;
import java.util.Map;
public class NamedParameterJdbcTemplateTest {
private ApplicationContext applicationContext;
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Before
public void init(){
applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
namedParameterJdbcTemplate = applicationContext.getBean(NamedParameterJdbcTemplate.class);
}
/**
* 使用具名參數時, 可以使用 update(String sql, SqlParameterSource paramSource) 方法進行更新操作
* 1. SQL 語句中的參數名和類的屬性一致!
* 2. 使用 SqlParameterSource 的 BeanPropertySqlParameterSource 實現類作爲參數.
*/
@Test
public void testBean(){
String sql = "insert into t_employee (id,last_name,email) values (:id,:lastName,:email)";
Employee employee = new Employee();
employee.setId(200);
employee.setLastName("erbai");
employee.setEmail("[email protected]");
//創建參數
SqlParameterSource paramSource = new BeanPropertySqlParameterSource(employee);
int row = namedParameterJdbcTemplate.update(sql, paramSource);
System.out.println(row);
}
/**
* 可以爲參數起名字.
* 1. 好處: 若有多個參數, 則不用再去對應位置, 直接對應參數名, 便於維護
* 2. 缺點: 較爲麻煩.
*/
@Test
public void testMap(){
String sql = "insert into t_employee (id,last_name,email,dept_id) values (:id,:lastName,:email,:deptId)";
Map<String,Object> paramMap = new HashMap<>();
paramMap.put("id",100);
paramMap.put("lastName","baige");
paramMap.put("email","[email protected]");
paramMap.put("deptId",2);
int row = namedParameterJdbcTemplate.update(sql, paramMap);
System.out.println(row);
}
}
3、Spring事務管理
3.1 簡介
事務管理是企業級應用程序開發中必不可少的技術,用來確保數據的完整性和一致性.。
事務就是一系列的動作,它們被當做一個單獨的工作單元,這些動作要麼全部完成, 要麼全部不起作用。
事務的四個關鍵屬性(ACID):
原子性(atomicity): 事務是一個原子操作, 由一系列動作組成;事務的原子性確保動作要麼全部完成,要麼完全不起作用;
一致性(consistency):一旦所有事務動作完成, 事務就被提交, 數據和資源就處於一種滿足業務規則的一致性狀態中;
隔離性(isolation):可能有許多事務會同時處理相同的數據,因此每個事物都應該與其他事務隔離開來,防止數據損壞;
持久性(durability):一旦事務完成,無論發生什麼系統錯誤,它的結果都不應該受到影響,通常情況下,事務的結果被寫到持久化存儲器中。
3.2 Spring 中的事務管理器
作爲企業級應用程序框架,Spring 在不同的事務管理 API 之上定義了一個抽象層。而應用程序開發人員不必瞭解底層的事務管理 API,就可以使用 Spring 的事務管理機制。
Spring 既支持編程式事務管理,也支持聲明式的事務管理。
編程式事務管理:將事務管理代碼嵌入到業務方法中來控制事務的提交和回滾,在編程式管理事務時, 必須在每個事務操作中包含額外的事務管理代碼。
聲明式事務管理:大多數情況下比編程式事務管理更好用, 它將事務管理代碼從業務方法中分離出來,以聲明的方式來實現事務管理。 事務管理作爲一種橫切關注點,可以通過 AOP 方法模塊化,Spring 通過 Spring AOP 框架支持聲明式事務管理
Spring 從不同的事務管理 API 中抽象了一整套的事務機制,開發人員不必瞭解底層的事務 API,就可以利用這些事務機制; 有了這些事務機制,事務管理代碼就能獨立於特定的事務技術了;
Spring 的核心事務管理抽象是 TransactionManager ,它爲事務管理封裝了一組獨立於技術的方法;無論使用 Spring 的哪種事務管理策略(編程式或聲明式),事務管理器都是必須的;
3.2.1 Spring 中的事務管理器的不同實現
1、DataSourceTransactionManager:在應用程序中只需要處理一個數據源,而且通過 JDBC 存取;
2、JtaTransactionManager:在 JavaEE 應用服務器上用 JTA(Java Transaction API) 進行事務管理;
3、HibernateTransactionManager:用 Hibernate 框架存取數據庫;
事務管理器以普通的 Bean 形式聲明在 Spring IOC 容器中
3.3 需求
以網上購書爲例:客戶購一本書,客戶的餘額要減少,書的庫存同時也要減少;這兩個操作應該要在同一個事務中。
創建數據庫表:
CREATE TABLE book (
isbn VARCHAR(20) PRIMARY KEY ,
book_name VARCHAR(50),
price INT
);
CREATE TABLE account (
username VARCHAR(20) ,
balance INT
);
CREATE TABLE book_stock (
isbn VARCHAR(20) PRIMARY KEY ,
stock INT
);
ALTER TABLE book_stock ADD CONSTRAINT fk_book_bs FOREIGN KEY (isbn) REFERENCES book(isbn);
INSERT INTO book VALUES ('0001','java',100);
INSERT INTO book VALUES ('0002','python',70);
INSERT INTO account VALUES ('tom',200);
INSERT INTO book_stock VALUES ('0001',10);
INSERT INTO book_stock VALUES ('0002',10);
3.4 基於註解的聲明式事務管理
Spring 還允許簡單地用 @Transactional 註解來標註事務方法;
爲了將方法定義爲支持事務處理的,可以爲方法添加 @Transactional 註解, 根據 Spring AOP 基於代理機制, 只能標註公有方法。
可以在方法或者類級別上添加 @Transactional 註解,當把這個註解應用到類上時, 這個類中的所有公共方法都會被定義成支持事務處理的。
在 Bean 配置文件中只需要啓用 <tx:annotation-driven> 元素,併爲之指定事務管理器就可以了。
如果事務處理器的名稱是 transactionManager, 就可以在<tx:annotation-driven> 元素中省略 transaction-manager 屬性,這個元素會自動檢測該名稱的事務處理器。
3.4.1 配置事務管理器
<!--配置事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
3.4.2 開啓註解事務
<!--開啓事務註解,如果事務管理器是名稱是transactionManager ,那麼可以不用配置transaction-manager="transactionManager" ,因爲是默認值-->
<tx:annotation-driven transaction-manager="transactionManager"/>
加上包掃描:
<!--配置包掃描,掃描@Repository @Service @Controller-->
<context:component-scan base-package="com.spring.jdbc,com.spring.tx"/>
3.4.3 代碼編寫
1、自定義兩個異常
package com.spring.tx.annotation;
//自定義庫存不足異常
public class BookStockException extends RuntimeException {
public BookStockException(String message) {
super(message);
}
}
package com.spring.tx.annotation;
//餘額不足的異常
public class UserAccountException extends RuntimeException {
public UserAccountException(String message) {
super(message);
}
}
2、定義dao接口和實現類
package com.spring.tx.annotation;
public interface BookShopDao {
//根據書號獲取書的單價
public int findBookPriceByIsbn(String isbn);
//更新數的庫存. 使書號對應的庫存 - 1
public void updateBookStock(String isbn);
//更新用戶的賬戶餘額: 使 username 的 balance - price
public void updateUserAccount(String username, int price);
}
package com.spring.tx.annotation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository("bookShopDao")
public class BookShopDaoImpl implements BookShopDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int findBookPriceByIsbn(String isbn) {
String sql = "select price from book where isbn = ?";
Integer price = jdbcTemplate.queryForObject(sql, Integer.class, isbn);
return price;
}
@Override
public void updateBookStock(String isbn) {
//檢查書的庫存是否足夠, 若不夠, 則拋出異常
String sql2 = "SELECT stock FROM book_stock WHERE isbn = ?";
int stock = jdbcTemplate.queryForObject(sql2, Integer.class, isbn);
if(stock == 0){
throw new BookStockException("庫存不足!");
}
String sql = "update book_stock set stock = stock - 1 where isbn = ?";
jdbcTemplate.update(sql,isbn);
}
@Override
public void updateUserAccount(String username, int price) {
//驗證餘額是否足夠, 若不足, 則拋出異常
String sql2 = "SELECT balance FROM account WHERE username = ?";
int balance = jdbcTemplate.queryForObject(sql2, Integer.class, username);
if(balance < price){
throw new UserAccountException("餘額不足!");
}
String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
jdbcTemplate.update(sql, price, username);
}
}
3、定義service接口和實現類
需要事務管理的方法purchase上面一定要添加@Transactional註解
package com.spring.tx.annotation;
public interface BookShopService {
//購買書本的方法
public void purchase(String username, String isbn);
}
package com.spring.tx.annotation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService{
@Autowired
private BookShopDao bookShopDao;
//添加事務註解
//1.使用 propagation 指定事務的傳播行爲, 即當前的事務方法被另外一個事務方法調用時
//如何使用事務, 默認取值爲 REQUIRED, 即使用調用方法的事務
//REQUIRES_NEW: 事務自己的事務, 調用的事務方法的事務被掛起.
//2.使用 isolation 指定事務的隔離級別, 最常用的取值爲 READ_COMMITTED
//3.默認情況下 Spring 的聲明式事務對所有的運行時異常進行回滾. 也可以通過對應的
//屬性進行設置. 通常情況下去默認值即可.
//4.使用 readOnly 指定事務是否爲只讀. 表示這個事務只讀取數據但不更新數據,
//這樣可以幫助數據庫引擎優化事務. 若真的事一個只讀取數據庫值的方法, 應設置 readOnly=true
//5.使用 timeout 指定強制回滾之前事務可以佔用的時間
@Transactional
@Override
public void purchase(String username, String isbn) {
//查詢書本單價
int price = bookShopDao.findBookPriceByIsbn(isbn);
//更新庫存
bookShopDao.updateBookStock(isbn);
//更新餘額
bookShopDao.updateUserAccount(username,price);
}
}
4、測試
package com.spring.tx.annotation;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TxAnnotationTest {
private ApplicationContext applicationContext;
private BookShopService bookShopService;
@Before
public void init(){
applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext.xml");
bookShopService = (BookShopService) applicationContext.getBean("bookShopService");
}
@Test
public void testAnnotationTx(){
bookShopService.purchase("tom","0001");
}
}
3.4.4 事務的傳播性
可以參考地址:https://segmentfault.com/a/1190000013341344
當事務方法被另一個事務方法調用時,必須指定事務應該如何傳播。例如: 方法可能繼續在現有事務中運行, 也可能開啓一個新事務,並在自己的事務中運行。
事務的傳播行爲可以由 propagation 傳播屬性指定, Spring 定義了 7 種類傳播行爲,他們在枚舉 Propagation 定義
事務傳播行爲類型 | 說明 |
---|---|
REQUIRED | 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。(默認值) |
SUPPORTS | 支持當前事務,如果當前沒有事務,就以非事務方式執行。 |
MANDATORY | 使用當前的事務,如果當前沒有事務,就拋出異常。 |
REQUIRES_NEW | 新建事務,如果當前存在事務,把當前事務掛起。 |
NOT_SUPPORTED | 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。 |
NEVER | 以非事務方式執行,如果當前存在事務,則拋出異常。 |
NESTED | 如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與REQUIRED類似的操作。 |
下面演示一下REQUIRED和REQUIRED_NEW:
需求:買多本書,用戶的結賬操作,但是用戶餘額不足以支付所有的費用
1、REQUIRED
定義 新的 Cashier 接口和實現類:
package com.spring.tx.annotation;
import java.util.List;
public interface Cashier {
//支付費用
public void checkout(String username, List<String> isbns);
}
package com.spring.tx.annotation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service("cashier")
public class CashierImpl implements Cashier {
@Autowired
private BookShopService bookShopService;
@Transactional
@Override
public void checkout(String username, List<String> isbns) {
for(String isbn: isbns){
bookShopService.purchase(username, isbn);
}
}
}
CashierImpl中的checkout方法上面添加了@Transactional註解,表明這是一個事務方法,同時默認的傳播屬性 propagation=REQUIRED;
測試方法如下:
//測試事務的傳播屬性:propagation = REQUIRED和propagation = REQUIRED_NEW
@Test
public void testPropagation(){
cashier.checkout("tom", Arrays.asList("0001","0002"));
}
如上,因爲我們的事務方法的傳播屬性都是REQUIRED,所以嵌套的事務方法會使用最外層的方法是事務,裏面只要有一個地方出現了異常,就會回滾整個事務。
當 bookService 的 purchase() 方法被另一個事務方法 checkout() 調用時, 它默認會在現有的事務內運行; 這個默認的傳播行爲就是 REQUIRED。因此在 checkout() 方法的開始和終止邊界內只有一個事務,這個事務只在 checkout() 方法結束的時候被提交, 結果用戶一本書都買不了。
2、REQUIRES_NEW
一種常見的傳播行爲是 REQUIRES_NEW,它表示該方法必須啓動一個新事務, 並在自己的事務內運行;如果有事務在運行, 就應該先掛起它。
修改之前的purchase方法,其他都不要改:
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void purchase(String username, String isbn) {
//查詢書本單價
int price = bookShopDao.findBookPriceByIsbn(isbn);
//更新庫存
bookShopDao.updateBookStock(isbn);
//更新餘額
bookShopDao.updateUserAccount(username,price);
}
請注意:@Transactional(propagation = Propagation.REQUIRES_NEW),我們把傳播屬性設置了,這樣的話,每次調用purchase方法,會掛起外部事務,只會在purchase方法的內部事務運行。如果我們的錢足夠買第一本書,那麼第一本書會購買成功,庫存和餘額都會正確的減少,但是買第二本的時候,會報餘額不足異常,購買失敗。
3.4.5 事務的隔離級別
髒讀:對於兩個事物T1, T2;T1讀取了已經被 T2 更新但還沒有被提交的字段,之後,若 T2回滾, T1讀取的內容就是臨時且無效的
不可重複讀:對於兩個事物 T1, T2; T1 讀取了一個字段, 然後 T2 更新了該字段, 之後,, T1再次讀取同一個字段, 值就不同了
虛讀(幻讀):對於兩個事物 T1, T2;T1 從一個表中讀取了一個字段, 然後 T2 在該表中插入了一些新的行, 之後, 如果 T1 再次讀取同一個表,就會多出幾行;
Spring支持五種事務隔離級別:
隔離級別 | 說明 |
DEFAULT |
這是一個PlatfromTransactionManager默認的隔離級別,使用數據庫默認的事務隔離級別, |
read uncommited |
允許事務讀取未被其他事務提交的變更;髒讀,不可重複讀,虛讀都有可能發生 |
read commited |
只允許事務讀取其他事務已經提交的變更;不可重複讀,虛讀有可能發生(Oracle默認事務隔離級別,開發時通常使用的隔離級別) |
repeatable read |
確保事務可以多次從一個字段中讀取相同的值,在這個事務持續期間,禁止其他事務對這個字段更新,可以避免髒讀,不可重複讀,但是虛讀還是有可能發生(MySQL默認事務隔離級別) |
serializable |
確保事務可以從一個表中讀取相同的行,在這個事務持續期間,禁止其他事務對該表執行插入、更新和刪除操作,可以避免髒讀,不可重複讀,虛讀,但是性能低下。 |
在@Transactional註解上,可以設置隔離級別屬性:isolation = Isolation.DEFAULT,他的可取值在枚舉類Isolation中定義。
事務的隔離級別要得到底層數據庫引擎的支持,而不是應用程序或者框架的支持。
Oracle 支持的 2 種事務隔離級別:READ_COMMITED(默認值) , SERIALIZABLE;
Mysql 支持 4 中事務隔離級別;
3.4.6 事務的回滾屬性
默認情況下只有未檢查異常(RuntimeException和Error類型的異常)會導致事務回滾,而受檢查異常不會。
事務的回滾規則可以通過 @Transactional 註解的 rollbackFor 和 noRollbackFor 屬性來定義,這兩個屬性被聲明爲 Class[] 類型的, 因此可以爲這兩個屬性指定多個異常類;例如:rollbackFor = {UserAccountException.class,BookStockException.class}
3.4.7 超時和只讀屬性
由於事務可以在行和表上獲得鎖, 因此長事務會佔用資源,並對整體性能產生影響; 如果一個事物只讀取數據但不做修改,數據庫引擎可以對這個事務進行優化。
超時事務屬性:事務在強制回滾之前可以保持多久, 這樣可以防止長期運行的事務佔用資源;通過 @Transactional 註解的timeout=3(單位是秒)
只讀事務屬性:表示這個事務只讀取數據但不更新數據, 這樣可以幫助數據庫引擎優化事務;通過 @Transactional 註解的readOnly = true
3.5 基於xml配置的聲明式事務管理
事務管理是一種橫切關注點
爲了在 Spring 2.x 中啓用聲明式事務管理,可以通過 tx Schema 中定義的 <tx:advice> 元素聲明事務通知,爲此必須事先將這個 Schema 定義添加到 <beans> 根元素中去;
聲明瞭事務通知後,就需要將它與切入點關聯起來。由於事務通知是在 <aop:config> 元素外部聲明的,所以它無法直接與切入點產生關聯,所以必須在 <aop:config> 元素中聲明一個增強器通知與切入點關聯起來。
由於 Spring AOP 是基於代理的方法,所以只能增強公共方法,因此,只有公有方法才能通過 Spring AOP 進行事務管理。
3.5.1 修改上述service類
首先,要把之前測試註解的那些類和接口,測試類以及自定義異常代碼,都複製一份到tx目錄下面:
修改CashierImpl和BookShopServiceImpl,BookShopDaoImpl,把這些個類中的@Service註解、@Autowired註解和@Transactional註解都刪掉,等會全部在xml配置文件中,通過<bean>進行配置。
凡是之前通過@Autowired進行自動注入的,都變成添加一個對應的set方法,然後在配置文件中進行注入,例如:
public class BookShopDaoImpl implements BookShopDao {
private JdbcTemplate jdbcTemplate;
//通過set方法注入JdbcTemplate
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
3.5.2 創建配置文件applicationContext-tx-xml.xml
創建一個新的配置文件,所有的IOC和DI,AOP等都在配置文件中,不使用註解了:
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
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/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--配置包掃描,掃描@Repository @Service @Controller-->
<context:component-scan base-package="com.spring.jdbc,com.spring.tx"/>
<!--引入外部數據源-->
<context:property-placeholder location="classpath:db.properties"/>
<!--配置數據源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"/>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
</bean>
<!--配置Spring的JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--他必須要配置一個數據源-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置Dao-->
<bean id="bookShopDao" class="com.spring.tx.xml.BookShopDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<!--配置bookShopService-->
<bean id="bookShopService" class="com.spring.tx.xml.service.impl.BookShopServiceImpl">
<property name="bookShopDao" ref="bookShopDao"/>
</bean>
<!--配置CashierImpl-->
<bean id="cashier" class="com.spring.tx.xml.service.impl.CashierImpl">
<property name="bookShopService" ref="bookShopService"/>
</bean>
<!--1 配置事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--2 配置事務屬性-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 根據方法名指定事務的屬性 -->
<tx:method name="purchase" propagation="REQUIRED"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 3. 配置事務切入點, 以及把事務切入點和事務屬性關聯起來 -->
<aop:config>
<aop:pointcut id="txPointCut" expression="execution(* com.spring.tx.xml.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
</beans>
其他基本上不用變,只要注意一點,我們的類中,導包要正確。