案例分析
本案例是圖書管理系統精簡部分,在數據庫中有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注解
