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條數據,至此,我們的目的也就實現了。