JDBC高階開發事務
1.事務的引入
1.1 轉賬業務
案例分析:之前給大家介紹過三層開發,相信大家對三層有了基本的瞭解,接下來我們就使用三層的開發模式來完成轉賬的案例1 首先在這裡,我們web層就直接通過main方法,直接填充資料,呼叫service層的業務邏輯。2 而service業務邏輯層主要完成轉賬的核心業務邏輯操作,包括轉出和轉入的2個核心操作。3 很明顯,轉入和轉出是和資料庫打交道的,所以需要將轉入和轉出這2個操作放在dao層。大體分工如下圖所示:
1.2 程式碼實現
(1)準備資料
create table account( id int primary key auto_increment, name varchar(20), money double ); insert into account values (null,'jack',10000); insert into account values (null,'rose',10000); insert into account values (null,'tom',10000);
c3p0-config.xml
<?xml version="1.0" encoding="UTF-8"?> <c3p0-config> <default-config> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb1</property> <property name="user">root</property> <property name="password">root</property> </default-config> </c3p0-config>
(2)dao層
public class AccountDao { /** * 在開發中,dao層的異常一般都拋,在service層抓 * @param outUser 付款人 * @param money 付款金額 * @throws SQLException */ public void outMoney(String outUser,int money) throws SQLException{ //1.建立QueryRunner物件 QueryRunner queryRunner = new QueryRunner(JdbcUtils.getDataSource()); //2.執行sql語句 String sql = "update account set money = money-? where name=?"; queryRunner.update(sql, money,outUser); } /** * 收款操作 * @param inUser 收款人的姓名 * @param money 收款金額 * @throws SQLException */ public void inMoney(String inUser,int money) throws SQLException{ //首先建立QueryRunner物件 QueryRunner queryRunner = new QueryRunner(JdbcUtils.getDataSource()); //執行sql語句 String sql = "update account set money = money+? where name=?"; queryRunner.update(sql, money,inUser); } }
(3)service層
public class AccountService {
/**
* 注意:我們需要保證service層的connection物件和dao層的是同一個.
* dao層不能關閉connection物件,connection物件的關閉放在servcie層中
* @param outUser
* @param inUser
* @param money
*/
public void transfer(String outUser,String inUser,int money){
AccountDao accountDao = new AccountDao();
try {
//付款
accountDao.outMoney(outUser, money);
//收款
accountDao.inMoney(inUser, money);
con.commit();
System.out.println("轉賬成功");
} catch (SQLException e) {
System.out.println("轉賬失敗");
e.printStackTrace();
}
}
}
(4)web層
public class WebTest {
public static void main(String[] args) {
String outUser = "jack";
String inUser = "rose";
int money = 100;
//直接呼叫service層的方法完成轉賬的操作
AccountService accountService = new AccountService();
accountService.transfer(outUser, inUser, money);
}
}
思考:在上面的案例中,在業務層裡面,假設在一個人付款成功,另一個人還沒有收款的時候,中間有其他業務操作的話,出現了異常,也就是在付款和收款之間加上如下圖所示程式碼,模擬轉賬的異常。
那麼這時候很明顯,程式終止,而後面收款的操作並不會去做,那麼另一個人就不會收到錢,而前一個人確確實實轉賬了。所以最終得出。上面的轉賬業務邏輯是存在bug的。那麼針對這樣的一個bug我們該如何去處理呢?
針對上面出現的問題的分析:上面的bug,因為異常的原因,導致了我們的一個完整的業務邏輯被中斷了。所以導致只執行了一條sql語句,後一條sql語句沒有去執行。所以,我們要保證,我們一個完整的業務邏輯要麼全部跑完,如果中間出現異常的話,為了保證最終的結果符合我們實際生活的一個情況,所以我們需要撤銷與當前業務邏輯有關的操作。所以這就需要學習接來下的事務來保證這樣一個情況。
2.事務概述
事務指的是邏輯上的一組操作,組成這組操作的各個單元要麼全都成功,要麼全都失敗.
事務作用:保證在一個事務中多次操作要麼全都成功,要麼全都失敗.
事務的出現 解決上面的問題。
1.事務是如何處理正常情況的呢?
2.事務是如何處理異常情況的呢?
2.1 mysql事務操作
MYSQL中可以有兩種方式進行事務的管理:
- 自動提交:Mysql預設自動提交,執行一條sql語句可提交一次事務
- 手動提交:先開啟,再提交
方式1:手動提交
-- 開啟事務
start transaction;
update account set money = 100;
select * from account;
-- 提交事務
commit;
-- 事務一旦提交就不能回滾了。
rollback;
start transaction;
update account set money = 100;
-- 回滾事務
rollback;
select * from account;
方式2:自動提交,通過修改mysql全域性變數“autocommit”進行控制
show variables like ‘%commit%’;
- 設定自動提交的引數為OFF:set autocommit = 0; – 0:OFF 1:ON
擴充套件:Oracle資料庫事務不自動提交
2.2 JDBC事務操作
注意:在jdbc事務操作用,事務的控制都是通過Connection物件完成的,當一個完整的業務操作前,我們首先使用connection.setAutoCommit(false)來開啟事務,當業務操作完成之後,我們需要使用connection.commit()來提交事務。當然了,如果出現了異常,我們需要撤銷所有的操作,所以出現異常,需要進行事務的回滾。
2.3 DBUtils事務操作
過程與jdbc操作一樣,之不過需要注意的是,connection必須手動控制,不能交給DBUtils去控制。
2.4 事務管理:傳遞Connection
修改service和dao,service將connection傳遞給dao,dao不需要自己獲得連線
很明顯,我們的事務是針對整個業務邏輯的。所以事務的管理應該在業務層進行管理。並且,需要注意的是,我們在整個事務的控制過程中應該是一個connection物件。所以,在業務層我們需要拿到connection物件,用connection物件對業務層進行事務處理。而既然在業務層獲取了connection物件,那麼就必須要保證我們在dao層拿到的connection物件和業務層拿到的connection物件是同一個,那麼這裡我們可以使用傳遞引數的方式將connection物件傳遞給dao層。
所以dao層需要修改,connection物件需要從業務層傳遞過來。並且需要注意的是,connection物件是不能在dao層關閉的,因為我們業務層還需要繼續使用connection物件。所以dao層程式碼的修改如下:
dao層:
public class AccountDao {
public void outMoney(Connection con,String outUser,int money) throws SQLException{
PreparedStatement prepareStatement = con.prepareStatement("update account set money=money-? where name=?");
prepareStatement.setInt(1, money);
prepareStatement.setString(2,outUser);
prepareStatement.executeUpdate();
JdbcUtils.release(null, null , prepareStatement);
}
public void inMoney(Connection con,String inUser,int money) throws SQLException{
PreparedStatement prepareStatement = con.prepareStatement("update account set money=money-? where name=?");
prepareStatement.setInt(1, money);
prepareStatement.setString(2,inUser);
prepareStatement.executeUpdate();
JdbcUtils.release(null, null , prepareStatement);
}
}
通過ThreadLocal來儲存connection
public class JdbcUtils {
//建立一個數據庫連線池
private static ComboPooledDataSource dataSource = new ComboPooledDataSource();
//建立一個threadLocal物件
public static ThreadLocal<Connection> local = new ThreadLocal<Connection>();
public static DataSource getDataSource(){
return dataSource;
}
//連線的獲取從資料庫連線池獲取
public static Connection getConnection(){
Connection con = null;
try {
//connection物件先從local中獲取
con = local.get();
if(con == null){
//表示第一次在當前執行緒中獲取connection物件。第一次獲取連線需要從資料庫連線池拿。
con = dataSource.getConnection();
local.set(con);
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return con;
}
}