JDBC事務控制管理
- 2020 年 2 月 14 日
- 筆記
今天是學習計劃的第二天,感覺自己的學習熱情還是很高漲的啊,那我們就趁熱打鐵,開始今天的學習。 今天的學習內容是JDBC的事務控制管理。 首先是概念性的內容 事務指邏輯上的一組操作,組成這組操作的各個單元,要麼全部成功,要麼全部不成功。這是我對於事務的理解。 舉個例子: A轉賬給B,對應如下的兩條sql語句 update from account set money = money – 100 where name = 『A』 update from account set money = money + 100 where name = 『B』 在現實生活中,這兩條sql語句要麼就應該同時成功,要麼就應該同時失敗,否則用戶的賬戶就會產生問題。 在MySQL數據庫中,默認情況下,一條sql語句就是一個單獨的事務,事務是自動提交的 在Oracle數據庫中,默認情況下,事務不是自動提交的,所有sql語句都處於一個事務中,需要手動進行事務提交。 數據庫事務命令
- start transaction 開啟事務
- rollback 回滾事務
- commit 提交事務
我們來操作一個案例感受一下。 首先設置賬戶表(account) 在表中插入一些初始數據
create table account( id int primary key not null auto_increment, name varchar(40), money double ); insert into account values(1,'aaa',1000); insert into account values(2,'bbb',1000); insert into account values(3,'ccc',1000);
然後打開我們的控制台進入數據庫,輸入 start transaction;
這時候我們去修改數據,輸入update account set money = money - 100 where name ='aaa';
,然後輸入查詢語句select * from account;
查詢

此時說明更新語句成功執行了,但是請注意,我們在更新數據之前使用start transaction語句開啟了一個事務,所以如果我們不手動提交,事務是不會被提交的,我們可以打開另一個控制台進入數據庫,並查詢該表

會發現,名字為aaa賬戶的餘額並沒有被改變,這就是事務的狀態。 其實,在事務管理中執行sql語句,都會使用數據庫內的臨時表保存,在沒有進行事務提交或者回滾的前提下,其它用戶是無法看到操作結果的。 回到第二個用戶的操作中,當我輸入commit;
提交事務時,我們再次查詢表數據

這個時候第二個用戶才能查詢到第一個用戶更新的數據。 我們繼續在第一個用戶的數據庫中操作,重新開啟一個事務,然後輸入update account set money = money + 100 where name ='bbb';
,查詢表數據

bbb賬戶的金額多了100,此時,我們可以使用回滾操作,通俗地講,就是撤銷上一次的sql語句,輸入rollback;
然後重新查詢表數據

會發現,bbb賬戶的金額又變為了1000,說明回滾操作生效了。 需要注意的是,sql語言中只有DML才能被事務管理,那什麼是DML,DML就是數據操縱語言,是SQL語言中,負責對數據庫對象運行數據訪問工作的指令集,以INSERT、UPDATE、DELETE三種指令為核心,分別代表插入、更新與刪除,所以只有這三個關鍵字才能被事務管理,其它語句是不能被事務管理的。 這樣事務的基本操作都在控制台進行了對應的練習,接下來我們了解一下JDBC在項目中是如何控制事務的。 在MyEclipse中新建一個名為demo的的web項目,然後導入數據庫的驅動,這個東西我就不提供了,在官網或者其它地方都能夠下載。為了方便接下來的數據庫操作,我先寫了一個簡易的JDBC工具類,新建JDBCUtils.java文件
/** * JDBC 工具類,抽取公共方法 * * @author seawind * */ public class JDBCUtils { private static final String DRIVERCLASS; private static final String URL; private static final String USER; private static final String PWD; static { ResourceBundle bundle = ResourceBundle.getBundle("dbconfig"); DRIVERCLASS = bundle.getString("DRIVERCLASS"); URL = bundle.getString("URL"); USER = bundle.getString("USER"); PWD = bundle.getString("PWD"); } // 建立連接 public static Connection getConnection() throws Exception { loadDriver(); return DriverManager.getConnection(URL, USER, PWD); } // 裝載驅動 private static void loadDriver() throws ClassNotFoundException { Class.forName(DRIVERCLASS); } // 釋放資源 public static void release(ResultSet rs, Statement stmt, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } rs = null; } release(stmt, conn); } public static void release(Statement stmt, Connection conn) { if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } stmt = null; } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } conn = null; } } }
還要有一個配置文件,因為在工具類中我是通過配置文件獲取屬性值進行數據庫連接的 注意,一定要在src目錄下新建dbconfig.properties文件
DRIVERCLASS=com.mysql.jdbc.Driver URL=jdbc:mysql:///test USER=root PWD=123456
新建TransferTest.java文件進行測試,我們先不用事務管理來編寫一下轉賬操作
@Test public void demo1(){ //模擬轉賬操作,先不使用事務管理 Connection conn = null; PreparedStatement stmt = null; try { conn = JDBCUtils.getConnection(); String sql1 = "update account set money = money - 100 where name = 'aaa'"; String sql2 = "update account set money = money + 100 where name = 'bbb'"; stmt = conn.prepareStatement(sql1); stmt.executeUpdate(); stmt = conn.prepareStatement(sql2); stmt.executeUpdate(); } catch (Exception e) { e.printStackTrace(); } }
先查詢一下數據庫表

此時三個用戶的賬戶餘額都為1000,現在運行剛才編寫的測試代碼,然後重新查詢表數據

賬戶餘額發生了對應的改變,說明測試代碼運行成功了。這裡沒有使用事務管理,兩句sql語句其實就是兩個事務,所以如果在兩個事務之間出現了一點問題,整個轉賬的過程就會出現意外,我們可以製造一點問題出來。
@Test public void demo1(){ //模擬轉賬操作,先不使用事務管理 Connection conn = null; PreparedStatement stmt = null; try { conn = JDBCUtils.getConnection(); String sql1 = "update account set money = money - 100 where name = 'aaa'"; String sql2 = "update account set money = money + 100 where name = 'bbb'"; stmt = conn.prepareStatement(sql1); stmt.executeUpdate(); //在兩個事務中間製造一個異常 int d = 1 / 0; stmt = conn.prepareStatement(sql2); stmt.executeUpdate(); } catch (Exception e) { e.printStackTrace(); } }
我們在兩個事務之間加上int d = 1 / 0;
的代碼,這樣就會在執行完第一條事務之後產生異常從而中斷程序運行,第二條事務也就得不到執行,看看是不是會發生這樣的事情。 我們運行測試代碼,程序報錯,然後查詢一下表數據

會發現,aaa用戶的賬戶餘額少了100,而bbb用戶的餘額並沒有被改變,顯然這種事情是不能被發生在現實生活中的銀行業務中的。在這種情況下,為了保證兩條sql語句的一致性,我們需要使用事務管理。在程序代碼中,JDBC是會自動提交我們的事務的,我們可以通過Connection
類的setAutoCommit(false)
方法來關閉自動提交,從而獲得控制事務提交的權利。 在TransferTest.java文件中編寫第二個測試方法
@Test public void demo2(){ //模擬轉賬操作,使用事務管理 Connection conn = null; PreparedStatement stmt = null; try { conn = JDBCUtils.getConnection(); //在連接獲得後,開啟事務 conn.setAutoCommit(false); //關閉自動提交 String sql1 = "update account set money = money - 100 where name = 'aaa'"; String sql2 = "update account set money = money + 100 where name = 'bbb'"; stmt = conn.prepareStatement(sql1); stmt.executeUpdate(); int d = 1 / 0; stmt = conn.prepareStatement(sql2); stmt.executeUpdate(); //如果程序能走到這一步 說明兩句sql都執行成功 提交事務 conn.commit(); } catch (Exception e) { //表示在執行轉賬的過程產生了異常 需要將兩句sql語句進行回滾 try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } }
現在,執行測試代碼,程序報錯,查詢表數據

三個賬戶的餘額均沒有發生變化,說明兩句sql語句被回滾了,當你刪掉錯誤代碼,重新運行,發現表數據被相應地修改了,這樣就達到了事務管理的目的。 這是基本的JDBC控制事務的方法了。 再來了解一些高級的事務操作,我們假設,當事務特別複雜的時候,有些情況不會回滾到事務的最開始狀態,這時候就需要將事務回滾到指定位置,此時就需要知道 事務回滾點(SavePoint)。 我們先創建一張person表
create table person( id int primary key, name varchar(40) );
新建一個測試方法
@Test public void demo3(){ //創建person表 向表中插入兩萬條數據 //如果插入過程中 發生錯誤 要保證插入的條數是1000的整數倍 Connection conn = null; PreparedStatement stmt = null; try { conn = JDBCUtils.getConnection(); String sql = "insert into person values(?,?)"; //預編譯sql stmt = conn.prepareStatement(sql); for(int i = 0;i <= 20000;i++){ stmt.setInt(1, i); stmt.setString(2, "name" + i); //添加到批處理 stmt.addBatch(); if(i == 4699){ int d = 1 / 0; } //每隔200次向數據庫發送一次 if(i % 200 == 0){ stmt.executeBatch(); stmt.clearBatch(); } } //為了確保緩存的sql都提交了 stmt.executeBatch(); } catch (Exception e) { e.printStackTrace(); }finally{ JDBCUtils.release(stmt, conn); } }
我們故意在循環內製造一個錯誤,運行測試代碼,程序報錯,查詢表數據

只有4600條數據,那麼該如何獲得1000的整數倍的數據記錄呢?我們可以在獲得連接之後獲得一個回滾點,然後在循環中每隔1000條數據就重新保存一下回滾點,然後在異常處理代碼塊中寫conn.rollback(savepoint);
回滾到回滾點。具體代碼如下:
@Test public void demo3(){ //創建person表 向表中插入兩萬條數據 //如果插入過程中 發生錯誤 要保證插入的條數是1000的整數倍 Connection conn = null; PreparedStatement stmt = null; Savepoint savepoint = null; try { conn = JDBCUtils.getConnection(); //開啟事務 conn.setAutoCommit(false); //保存一次回滾點 savepoint = conn.setSavepoint(); String sql = "insert into person values(?,?)"; //預編譯sql stmt = conn.prepareStatement(sql); for(int i = 1;i <= 20000;i++){ stmt.setInt(1, i); stmt.setString(2, "name" + i); //添加到批處理 stmt.addBatch(); if(i == 4699){ int d = 1 / 0; } //每隔200次向數據庫發送一次 if(i % 200 == 0){ stmt.executeBatch(); stmt.clearBatch(); } if(i % 1000 == 0){//1000的整數倍 //保存回滾點 savepoint = conn.setSavepoint(); } } //為了確保緩存的sql都提交了 //如果執行到這 說明程序沒有錯誤 conn.commit(); stmt.executeBatch(); } catch (Exception e) { //回滾事務 回滾到存儲點 try { conn.rollback(savepoint); conn.commit(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); }finally{ JDBCUtils.release(stmt, conn); } }
先在數據庫控制台輸入truncate person;
來清除person表中的所有數據,然後執行測試代碼,查詢表數據

會發現,當前只有4000條數據了,因為程序出現異常,事務記錄了第4000條記錄的回滾點,並在出現異常之後回滾到了第4000條數據,至此,我們的目的也就實現了。