Spring 學習(二十三)——宣告式事務
事務簡介
•事務管理是企業級應用程式開發中必不可少的技術, 用來確保資料的完整性和一致性.
•事務就是一系列的動作, 它們被當做一個單獨的工作單元. 這些動作要麼全部完成, 要麼全部不起作用
•事務的四個關鍵屬性(ACID)
–原子性(atomicity): 事務是一個原子操作, 由一系列動作組成. 事務的原子性確保動作要麼全部完成要麼完全不起作用.
–一致性(consistency): 一旦所有事務動作完成, 事務就被提交. 資料和資源就處於一種滿足業務規則的一致性狀態中.
–隔離性(isolation): 可能有許多事務會同時處理相同的資料, 因此每個事物都應該與其他事務隔離開來
–永續性(durability): 一旦事務完成, 無論發生什麼系統錯誤, 它的結果都不應該受到影響. 通常情況下, 事務的結果被寫到持久化儲存器中.
事務管理的問題
•問題:
–必須為不同的方法重寫類似的樣板程式碼
–這段程式碼是特定於 JDBC 的, 一旦選擇類其它資料庫存取技術, 程式碼需要作出相應的修改
Spring 中的事務管理
•作為企業級應用程式框架, Spring 在不同的事務管理 API 之上定義了一個抽象層. 而應用程式開發人員不必瞭解底層的事務管理 API, 就可以使用
•Spring 既支援程式設計式事務管理, 也支援宣告式的事務管理.
•程式設計式事務管理: 將事務管理程式碼嵌入到業務方法中來控制事務的提交和回滾. 在程式設計式管理事務時, 必須在每個事務操作中包含額外的事務管理程式碼.
•宣告式事務管理: 大多數情況下比程式設計式事務管理更好用. 它將事務管理程式碼從業務方法中分離出來, 以宣告的方式來實現事務管理. 事務管理作為一種橫切關注點, 可以通過 AOP 方法模組化. Spring 通過 Spring AOP 框架支援宣告式事務管理.
•Spring 從不同的事務管理 API 中抽象了一整套的事務機制
•Spring 的核心事務管理抽象是 它為事務管理封裝了一組獨立於技術的方法. 無論使用 Spring 的哪種事務管理策略(程式設計式或宣告式), 事務管理器都是必須的.
Spring 中的事務管理器的不同實現
需求
資料表中的資料
用事務通知宣告式地管理事務
•事務管理是一種橫切關注點
•為了在 Spring 2.x 中啟用宣告式事務管理, 可以通過 tx Schema 中定義的 <tx:advice> 元素宣告事務通知, 為此必須事先將這個 Schema 定義新增到 <beans> 根元素中去.
•聲明瞭事務通知後, 就需要將它與切入點關聯起來. 由於事務通知是在 <aop:config> 元素外部宣告的, 所以它無法直接與切入點產生關聯. 所以必須在 <aop:config> 元素中宣告一個增強器通知與切入點關聯起來.
•由於 Spring AOP 是基於代理的方法, 所以只能增強公共方法. 因此, 只有公有方法才能通過 Spring AOP 進行事務管理.
用事務通知宣告式地管理事務示例程式碼
用 @Transactional 註解宣告式地管理事務
•除了在帶有切入點, 通知和增強器的 Bean 配置檔案中宣告事務外, Spring 還允許簡單地用 @Transactional 註解來標註事務方法.
•為了將方法定義為支援事務處理的, 可以為方法新增 @Transactional 註解. 根據 Spring AOP 基於代理機制, 只能標註公有方法.
•可以在方法或者類級別上新增 @Transactional 註解. 當把這個註解應用到類上時, 這個類中的所有公共方法都會被定義成支援事務處理的.
•在 Bean 配置檔案中只需要啟用 <tx:annotation-driven> 元素, 併為之指定事務管理器就可以了.
•如果事務處理器的名稱是 transactionManager, 就可以在<tx:annotation-driven> 元素中省略 transaction-manager 屬性. 這個元素會自動檢測該名稱的事務處理器.
用 @Transactional 註解宣告式地管理事務配置檔案示例程式碼
程式碼示例:
1、資料庫表的建立如下:
employee表
department表
2、目錄結構
Spring
|——src
|——|——com.hzyc.spring.jdbc
|——|——|——employee.java
|——|——|——JdbcTest.java
|——|——application-context.xml
|——|——db.properties
2、db.properties
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.jdbcUrl=jdbc:mysql:///spring
jdbc.user=root
jdbc.password=root
jdbc.initPoolSize=5
jdbc.maxPoolSize=10
3、application-context.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: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-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:component-scan base-package="com.hzyc.spring"/>
<!--匯入資原始檔-->
<context:property-placeholder location="classpath:db.properties"/>
<!--配置 c3p0 資料來源-->
<bean id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driverClass}"/>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
<property name="user" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"/>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
</bean>
<!--配置 Spring 的 Jdbc Template-->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--
配置 NamedParameterJdbcTemplate, 該物件可以使用具名引數(具有名字)
其沒有無參的構造器,必須為其構造器指定引數
-->
<bean id="namedParameterJdbcTemplate"
class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"/>
</bean>
<!--配置事務管理器-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--事務註解-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
bookShopDao.java
package com.hzyc.spring.jdbc.transaction;
/**
* @author xuehj2016
* @Title: BookShopDao
* @ProjectName Spring
* @Description: TODO
* @date 2018/12/21 20:32
*/
public interface BookShopDao {
/**
* 根據書號獲取書的單價
*/
public int getBookPriceById(String id);
/**
* 更新書的庫存,使書號對應的庫存 - 1
*/
public void updateBookStock(String id);
/**
* 更新使用者的賬戶餘額 : 使 username 的 balance - price
*/
public void updateUserAccount(String username, int price);
}
BookShopDaoImpl.java
package com.hzyc.spring.jdbc.transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
/**
* @author xuehj2016
* @Title: BookShopDaoImpl
* @ProjectName Spring
* @Description: TODO
* @date 2018/12/21 20:39
*/
@Repository("bookShopDao")
public class BookShopDaoImpl implements BookShopDao {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 根據書號獲取書的單價
*
* @param id
*/
@Override
public int getBookPriceById(String id) {
String sql = "select price from book where id = ?";
return jdbcTemplate.queryForObject(sql, Integer.class, id);
}
/**
* 更新書的庫存,使書號對應的庫存 - 1
*
* @param id
*/
@Override
public void updateBookStock(String id) {
//檢查書的庫存是否足夠,若不夠,則丟擲異常
String stockSql = "select stock from book_stock where id =?";
int stock = jdbcTemplate.queryForObject(stockSql, Integer.class, id);
if (stock == 0) {
throw new BookStockException("庫存不足!");
}
String sql = "update book_stock set stock = stock - 1 where id =?";
jdbcTemplate.update(sql, id);
}
/**
* 更新使用者的賬戶餘額 : 使 username 的 balance - price
*
* @param username
* @param price
*/
@Override
public void updateUserAccount(String username, int price) {
//因為 MySQL 不支援檢查約束
//驗證使用者的餘額是否足夠,若不夠,則丟擲異常
String balanceSql = "select balance from account where username =?";
int balance = jdbcTemplate.queryForObject(balanceSql, Integer.class, username);
if (balance < price) {
throw new UserAccountException("餘額不足!");
}
String sql = "update account set balance = balance - ? where username =?";
jdbcTemplate.update(sql, price, username);
}
}
bookshopservice.java
package com.hzyc.spring.jdbc.transaction;
/**
* @author xuehj2016
* @Title: BookShopService
* @ProjectName Spring
* @Description: TODO
* @date 2018/12/21 21:22
*/
public interface BookShopService {
public void purchase(String username, String id);
}
BookShopServiceImpl.java
package com.hzyc.spring.jdbc.transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author xuehj2016
* @Title: BookShopServiceImpl
* @ProjectName Spring-3
* @Description: TODO
* @date 2018/12/21 21:24
*/
@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService {
@Autowired
private BookShopDao bookShopDao;
/**
* 新增事務註解
*
* @param username
* @param id
*/
@Transactional
@Override
public void purchase(String username, String id) {
//1.獲取書的單價
int price = bookShopDao.getBookPriceById(id);
//2.更新書的庫存
bookShopDao.updateBookStock(id);
//3.更新使用者餘額
bookShopDao.updateUserAccount(username, price);
}
}
BookStockException.java
package com.hzyc.spring.jdbc.transaction;
/**
* @author xuehj2016
* @Title: BookStockException
* @ProjectName Spring
* @Description: TODO
* @date 2018/12/21 21:10
*/
public class BookStockException extends RuntimeException {
public BookStockException() {
}
public BookStockException(String message) {
super(message);
}
public BookStockException(String message, Throwable cause) {
super(message, cause);
}
public BookStockException(Throwable cause) {
super(cause);
}
public BookStockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
useraccountException.java
package com.hzyc.spring.jdbc.transaction;
/**
* @author xuehj2016
* @Title: UserAccountException
* @ProjectName Spring
* @Description: TODO
* @date 2018/12/21 21:16
*/
public class UserAccountException extends RuntimeException {
public UserAccountException() {
}
public UserAccountException(String message) {
super(message);
}
public UserAccountException(String message, Throwable cause) {
super(message, cause);
}
public UserAccountException(Throwable cause) {
super(cause);
}
public UserAccountException(String message, Throwable cause, boolean enableSuppression, boolean
writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
springTransactionTest.java
package com.hzyc.spring.jdbc.transaction;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author xuehj2016
* @Title: SpringTransactionTest
* @ProjectName Spring
* @Description: TODO
* @date 2018/12/21 20:52
*/
public class SpringTransactionTest {
private BookShopDao bookShopDao;
private BookShopService bookShopService;
{
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application-context.xml");
bookShopDao = (BookShopDao) applicationContext.getBean("bookShopDao");
bookShopService = (BookShopService) applicationContext.getBean("bookShopService");
}
@Test
public void testBookShopGetBookPriceById() {
System.out.println(bookShopDao.getBookPriceById("1001"));
}
@Test
public void testBookShopUpdateBookStock() {
bookShopDao.updateBookStock("1001");
}
@Test
public void testBookShopUpdateUserAccount() {
bookShopDao.updateUserAccount("xuehj", 50);
}
@Test
public void testBookShopService() {
bookShopService.purchase("xuehj","1001");
}
}