Spring 聲明式事務管理(11)
案例分析
本案例是圖書管理系統精簡部分,在數據庫中有3張表。分別保存圖書庫存、圖書信息和用戶信息。下面是建表SQL語句
1 DROP TABLE IF EXISTS store;
2 DROP TABLE IF EXISTS book ;
3 DROP TABLE IF EXISTS user;
4
5 -- 圖書表
6 CREATE TABLE book(
7 sn VARCHAR(20) PRIMARY KEY , -- 圖書編碼
8 name VARCHAR(20) NOT NULL, -- 圖書名稱
9 price NUMERIC(9,2) NOT NULL -- 圖書價格
10 );
11
12 -- 倉庫表
13 CREATE TABLE store(
14 sn VARCHAR(20), -- 圖書編碼
15 stock INT(9) NOT NULL, -- 圖書庫存
16 CONSTRAINT fk_sn FOREIGN KEY (sn) REFERENCES book(sn)
17 );
18
19 -- 用戶表
20 CREATE TABLE user(
21 id INT(11) PRIMARY KEY AUTO_INCREMENT, -- id
22 name VARCHAR(20) NOT NULL, -- 姓名
23 balance NUMERIC(9,2) NOT NULL DEFAULT 0 -- 余額
24 );
25
26 INSERT INTO book VALUES (‘1001‘,‘Java從入門到精通‘,100);
27 INSERT INTO book VALUES (‘1002‘,‘Spring從入門到精通‘,90);
28 INSERT INTO book VALUES (‘1003‘,‘J2EE核心框架‘,80);
29
30 INSERT INTO store VALUES (‘1001‘,50);
31 INSERT INTO store VALUES (‘1002‘,20);
32 INSERT INTO store VALUES (‘1003‘,10);
33
34 INSERT INTO user (name,balance) VALUES (‘caoyc‘,150);
實體類
Book.java
1 package com.proc.bean;
2
3 public class Book {
4
5 private String sn;
6 private String name;
7 private Double price;
8
9 public String getSn() {
10 return sn;
11 }
12
13 public void setSn(String sn) {
14 this.sn = sn;
15 }
16
17 public String getName() {
18 return name;
19 }
20
21 public void setName(String name) {
22 this.name = name;
23 }
24
25 public Double getPrice() {
26 return price;
27 }
28
29 public void setPrice(Double price) {
30 this.price = price;
31 }
32
33 public String toString() {
34 return "Book [sn=" + sn + ", name=" + name + ", price=" + price + "]";
35 }
36
37 }
Store.java
1 package com.proc.bean;
2
3 /**倉庫類*/
4 public class Store {
5
6 private String sn;
7 private Integer stock;
8 public String getSn() {
9 return sn;
10 }
11 public void setSn(String sn) {
12 this.sn = sn;
13 }
14 public Integer getStock() {
15 return stock;
16 }
17 public void setStock(Integer stock) {
18 this.stock = stock;
19 }
20 @Override
21 public String toString() {
22 return "Store [sn=" + sn + ", stock=" + stock + "]";
23 }
24
25
26 }
User.java
1 package com.proc.bean;
2
3 /**
4 * @author caoyc
5 *
6 */
7 public class User {
8
9 private Integer id;
10 private String name;
11 private Double balance;
12 public Integer getId() {
13 return id;
14 }
15 public void setId(Integer id) {
16 this.id = id;
17 }
18 public String getName() {
19 return name;
20 }
21 public void setName(String name) {
22 this.name = name;
23 }
24 public Double getBalance() {
25 return balance;
26 }
27 public void setBalance(Double balance) {
28 this.balance = balance;
29 }
30 @Override
31 public String toString() {
32 return "User [id=" + id + ", name=" + name + ", balance=" + balance
33 + "]";
34 }
35
36
37 }
Spring配置信息
使用db.properties記錄數據庫配置信息,這樣便於後期維護
1 jdbc.user=root
2 jdbc.password=123456
3 jdbc.driverClass=com.mysql.jdbc.Driver
4 jdbc.jdbcUrl=jdbc\:mysql\:///test
配置applicationContext.xml信息
1 <!-- 配置自動掃描策略 -->
2 <context:component-scan base-package="com.proc"/>
3
4 <!-- 讀取db.properties配置信息 -->
5 <context:property-placeholder location="classpath:db.properties"/>
6
7 <!-- 配置一個C3P0數據源 -->
8 <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
9 <property name="user" value="${jdbc.user}"/>
10 <property name="password" value="${jdbc.password}"/>
11 <property name="driverClass" value="${jdbc.driverClass}"/>
12 <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
13 </bean>
14
15 <!-- 配置一個JdbcTemplate,用來操作數據庫 -->
16 <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
17 <property name="dataSource" ref="dataSource"/>
18 </bean>
這裏我們使用自動掃描策略,使用的是C3P0連接池。同時使用了Spring內置的jdbc封裝類JdbcTemplate
數據訪問層
Java代碼:BookDao.java
1 package com.proc.dao;
2
3 import org.springframework.beans.factory.annotation.Autowired;
4 import org.springframework.jdbc.core.BeanPropertyRowMapper;
5 import org.springframework.jdbc.core.JdbcTemplate;
6 import org.springframework.jdbc.core.RowMapper;
7 import org.springframework.stereotype.Repository;
8
9 import com.proc.bean.Book;
10
11 /**圖書Dao*/
12 @Repository
13 public class BookDao {
14
15 @Autowired
16 private JdbcTemplate jdbcTemplate;
17
18 /**通過圖書編號獲取圖書信息*/
19 public Book get(String sn){
20
21 String sql="SELECT * FROM book WHERE sn=?";
22 RowMapper<Book> rowMapper=new BeanPropertyRowMapper<Book>(Book.class);
23 Book book=jdbcTemplate.queryForObject(sql, rowMapper,sn);
24 return book;
25 }
26 }
StoreDao
1 package com.proc.dao;
2
3 import org.springframework.beans.factory.annotation.Autowired;
4 import org.springframework.jdbc.core.BeanPropertyRowMapper;
5 import org.springframework.jdbc.core.JdbcTemplate;
6 import org.springframework.jdbc.core.RowMapper;
7 import org.springframework.stereotype.Repository;
8
9 import com.proc.bean.Store;
10
11 /**圖書倉庫Dao*/
12 @Repository
13 public class StoreDao {
14
15 @Autowired
16 private JdbcTemplate jdbcTemplate;
17
18 /**通過圖書編號獲取圖書庫存信息*/
19 public Store get(String sn){
20 String sql="SELECT * FROM store WHERE sn=?";
21 RowMapper<Store> rowMapper=new BeanPropertyRowMapper<Store>(Store.class);
22 Store store=jdbcTemplate.queryForObject(sql, rowMapper,sn);
23 return store;
24 }
25
26 /**通過圖書編號,修改圖書庫存 庫存=當前庫存-1*/
27 public void update(String sn){
28 String sql="UPDATE store SET stock=stock-1 WHERE sn=?";
29 jdbcTemplate.update(sql, sn);
30 }
31 }
UserDao.java
1 package com.proc.dao;
2
3 import org.springframework.beans.factory.annotation.Autowired;
4 import org.springframework.jdbc.core.BeanPropertyRowMapper;
5 import org.springframework.jdbc.core.JdbcTemplate;
6 import org.springframework.jdbc.core.RowMapper;
7 import org.springframework.stereotype.Repository;
8
9 import com.proc.bean.User;
10
11 /**用戶Dao*/
12 @Repository
13 public class UserDao {
14
15 @Autowired
16 private JdbcTemplate jdbcTemplate;
17
18 /**通過用戶ID獲取用戶信息*/
19 public User get(Integer id){
20 String sql="SELECT * FROM user WHERE id=?";
21 RowMapper<User> rowMapper=new BeanPropertyRowMapper<User>(User.class);
22 User user=jdbcTemplate.queryForObject(sql, rowMapper,id);
23 return user;
24 }
25
26 /**修改用戶余額*/
27 public void update(Integer id,Double price){
28 String sql="UPDATE user SET balance=balance-? WHERE id=?";
29 jdbcTemplate.update(sql, new Object[]{price,id});
30 }
31 }
這裏為每個Dao都註入了一個JdbcTemplate的實例,可以使用JDBC方式操作數據庫
異常處理
考慮到有可能會出現用戶余額或圖書庫存不足的情況,這裏我們自定義了兩個異常
1、庫存不足異常類:
1 package com.proc.exception;
2
3 public class BookStockException extends RuntimeException{
4 public BookStockException(String msg) {
5 super(msg);
6 }
7 }
2、余額不足異常類
1 package com.proc.exception;
2
3 public class UserBalanceException extends RuntimeException {
4 public UserBalanceException(String msg) {
5 super(msg);
6 }
7 }
邏輯業務層
1、定義一個接口
1 package com.proc.service;
2
3 public interface BookShopService {
4
5 /**
6 * 購買圖書
7 * @param userId 購買用戶ID
8 * @param sn 圖書編號
9 */
10 void purchase(Integer userId,String sn);
11 }
2、定義一個BookShopService借口實現類
1 package com.proc.service;
2
3 import org.springframework.beans.factory.annotation.Autowired;
4 import org.springframework.stereotype.Service;
5 import org.springframework.transaction.annotation.Transactional;
6
7 import com.proc.bean.Book;
8 import com.proc.bean.Store;
9 import com.proc.bean.User;
10 import com.proc.dao.BookDao;
11 import com.proc.dao.StoreDao;
12 import com.proc.dao.UserDao;
13 import com.proc.exception.BookStockException;
14 import com.proc.exception.UserBalanceException;
15
16 @Service("bookShopService")
17 public class BookShopServiceJdbcImps implements BookShopService{
18
19 @Autowired
20 private UserDao userDao;
21 @Autowired
22 private BookDao bookDao;
23 @Autowired
24 private StoreDao storeDao;
25
26
27 /**購買圖書方法*/
28 public void purchase(Integer userId, String sn) {
29
30 //1:查收出圖庫存信息
31 Store store= storeDao.get(sn);
32 if(store.getStock()<=0){
33 throw new BookStockException("圖書庫存不足:"+store);
34 }
35
36 //2:查詢圖書信息
37 Book book=bookDao.get(sn);
38
39
40 //3:查詢用戶信息
41 User user=userDao.get(userId);
42 if(user.getBalance()<book.getPrice()){
43 throw new UserBalanceException("用戶余額不足:"+user);
44 }
45
46 //4:修改庫存
47 storeDao.update(sn);
48
49 //5:修改余額
50 userDao.update(userId, book.getPrice());
51 }
52
53 }
測試代碼:
1 package com.proc.test;
2
3 import org.junit.Test;
4 import org.springframework.context.ApplicationContext;
5 import org.springframework.context.support.ClassPathXmlApplicationContext;
6
7 import com.proc.service.BookShopService;
8
9 public class TestShopBook {
10
11 private ApplicationContext ctx;
12 private BookShopService bookShopService;
13 {
14 ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
15 bookShopService=(BookShopService) ctx.getBean("bookShopService");
16 }
17
18 @Test
19 public void purchase(){
20
21 bookShopService.purchase(1, "1001");
22 }
23 }
第一次執行:
可以成功的看到數據是符合要求的
第二次執行:
我們看到會拋出異常。由於余額50元以不夠買價格為100元的1001編號書籍。所有拋出異常。我們看看數據庫中結果怎麽樣
看起來數據是正確的。由於余額不足,那麽購買不成功,所有庫存和金額都不會變好。那是不是使用了事務呢?
答案是:沒有。這裏沒有使用事務。只是因為我們先判斷了圖書庫和用戶余額是否足夠,然後再執行的修改信息。如果要測試代碼。我們將我們邏輯業務層代碼中第4步放到第2步前面執行
1 //1:查收出圖庫存信息
2 Store store= storeDao.get(sn);
3 if(store.getStock()<=0){
4 throw new BookStockException("圖書庫存不足:"+store);
5 }
6
7 //4:修改庫存
8 storeDao.update(sn);
9
10 //2:查詢圖書信息
11 Book book=bookDao.get(sn);
12
13
14 //3:查詢用戶信息
15 User user=userDao.get(userId);
16 if(user.getBalance()<book.getPrice()){
17 throw new UserBalanceException("用戶余額不足:"+user);
18 }
再次執行代碼:
雖然在此時還是或拋出余額不足的異常。但是庫存卻改變了。余額沒有改變。所有不滿足事務的要求。
那麽要怎麽辦呢?
1、在spring配置文件中配置事務管理器
1 <!-- 配置事務管理器 -->
2 <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
3 <property name="dataSource" ref="dataSource"></property>
4 </bean>
5
6 <!-- 使得事務註解生效 -->
7 <tx:annotation-driven transaction-manager="transactionManager"/>
事務管理器需要註入一個DataSource接口類型的數據源
2、在需要使用事務管理的方法前加上@Transactional註解
1 @Transactional
2 /**購買圖書方法*/
3 public void purchase(Integer userId, String sn) {
4
5 //1:查收出圖庫存信息
6 Store store= storeDao.get(sn);
7 if(store.getStock()<=0){
8 throw new BookStockException("圖書庫存不足:"+store);
9 }
10
11 //4:修改庫存
12 storeDao.update(sn);
13
14 //2:查詢圖書信息
15 Book book=bookDao.get(sn);
16
17
18 //3:查詢用戶信息
19 User user=userDao.get(userId);
20 if(user.getBalance()<book.getPrice()){
21 throw new UserBalanceException("用戶余額不足:"+user);
22 }
23
24 //5:修改余額
25 userDao.update(userId, book.getPrice());
26 }
再次執行代碼:
雖然還是拋出了異常。但是庫存和余額都沒有發生變化。這裏證明是使用了事務
【總結】:基於聲明式的事務就是上面用的這種方法
第一步:在spring配置中配置事務管理器
第二步:在需要使用事務的方法前面加上@Transactional註解
Spring 聲明式事務管理(11)