Java事务处理全解析(二)——失败的案例
在本系列的上一篇文章中,我們講到了Java事務處理的基本問題,并且講到了Service層和DAO層,在本篇文章中,我們將以BankService為例學習一個事務處理失敗的案例。
?
BankService的功能為:某個用戶有兩個賬戶,分別為銀行賬戶和保險賬戶,并且有各自的賬戶號,BankService的transfer方法從該用戶的銀行賬戶向保險賬戶轉帳,兩個DAO分別用于對兩個賬戶表的存取操作。
定義一個BankService接口如下:
package davenkin;public interface BankService {public void transfer(int fromId, int toId, int amount); }?
在兩個DAO對象中,我們通過傳入的同一個DataSource獲得Connection,然后通過JDBC提供的API直接對數據庫進行操作。
定義操作銀行賬戶表的DAO類如下:
package davenkin.step1_failure;import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException;public class FailureBankDao {private DataSource dataSource;public FailureBankDao(DataSource dataSource) {this.dataSource = dataSource;}public void withdraw(int bankId, int amount) throws SQLException {Connection connection = dataSource.getConnection();PreparedStatement selectStatement = connection.prepareStatement("SELECT BANK_AMOUNT FROM BANK_ACCOUNT WHERE BANK_ID = ?");selectStatement.setInt(1, bankId);ResultSet resultSet = selectStatement.executeQuery();resultSet.next();int previousAmount = resultSet.getInt(1);resultSet.close();selectStatement.close();int newAmount = previousAmount - amount;PreparedStatement updateStatement = connection.prepareStatement("UPDATE BANK_ACCOUNT SET BANK_AMOUNT = ? WHERE BANK_ID = ?");updateStatement.setInt(1, newAmount);updateStatement.setInt(2, bankId);updateStatement.execute();updateStatement.close();connection.close();} }?
FailureBankDao的withdraw方法,從銀行賬戶表(BANK_ACCOUNT)中帳號為bankId的用戶賬戶中取出數量為amount的金額。
采用同樣的方法,定義保險賬戶的DAO類如下:
package davenkin.step1_failure;import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException;public class FailureInsuranceDao {private DataSource dataSource;public FailureInsuranceDao(DataSource dataSource){this.dataSource = dataSource;}public void deposit(int insuranceId, int amount) throws SQLException {Connection connection = dataSource.getConnection();PreparedStatement selectStatement = connection.prepareStatement("SELECT INSURANCE_AMOUNT FROM INSURANCE_ACCOUNT WHERE INSURANCE_ID = ?");selectStatement.setInt(1, insuranceId);ResultSet resultSet = selectStatement.executeQuery();resultSet.next();int previousAmount = resultSet.getInt(1);resultSet.close();selectStatement.close();int newAmount = previousAmount + amount;PreparedStatement updateStatement = connection.prepareStatement("UPDATE INSURANCE_ACCOUNT SET INSURANCE_AMOUNT = ? WHERE INSURANCE_ID = ?");updateStatement.setInt(1, newAmount);updateStatement.setInt(2, insuranceId);updateStatement.execute();updateStatement.close();connection.close();} }?
FailureInsuranceDao類的deposit方法向保險賬戶表(INSURANCE_ACCOUNT)存入amount數量的金額,這樣在BankService中,我們可以先調用FailureBankDao的withdraw方法取出一定金額的存款,再調用FailureInsuranceDao的deposit方法將該筆存款存入保險賬戶表中,一切看似OK,實現BankService接口如下:
package davenkin.step1_failure;import davenkin.BankService;import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException;public class FailureBankService implements BankService{private FailureBankDao failureBankDao;private FailureInsuranceDao failureInsuranceDao;private DataSource dataSource;public FailureBankService(DataSource dataSource) {this.dataSource = dataSource;}public void transfer(int fromId, int toId, int amount) {Connection connection = null;try {connection = dataSource.getConnection();connection.setAutoCommit(false);failureBankDao.withdraw(fromId, amount);failureInsuranceDao.deposit(toId, amount);connection.commit();} catch (Exception e) {try {assert connection != null;connection.rollback();} catch (SQLException e1) {e1.printStackTrace();}} finally {try{assert connection != null;connection.close();} catch (SQLException e){e.printStackTrace();}}}public void setFailureBankDao(FailureBankDao failureBankDao) {this.failureBankDao = failureBankDao;}public void setFailureInsuranceDao(FailureInsuranceDao failureInsuranceDao) {this.failureInsuranceDao = failureInsuranceDao;} }?
在FailureBankService的transfer方法中,我們首先通過DataSource獲得Connection,然后調用connection.setAutoCommit(false)已開啟手動提交模式,如果一切順利,則commit,如果出現異常,則rollback。 接下來,開始測試我們的BankService吧。
為了準備測試數據,我們定義個BankFixture類,該類負責在每次測試之前準備測試數據,分別向銀行賬戶(1111)和保險賬戶(2222)中均存入1000元。BankFixture還提供了兩個helper方法(getBankAmount和getInsuranceAmount)幫助我們從數據庫中取出數據以做數據驗證。我們使用HSQL數據庫的in-memory模式,這樣不用啟動數據庫server,方便測試。BankFixture類定義如下:
package davenkin;import org.junit.Before; import javax.sql.DataSource; import java.sql.*;public class BankFixture {protected final DataSource dataSource = DataSourceFactory.createDataSource();@Beforepublic void setUp() throws SQLException{Connection connection = dataSource.getConnection();Statement statement = connection.createStatement();statement.execute("DROP TABLE BANK_ACCOUNT IF EXISTS");statement.execute("DROP TABLE INSURANCE_ACCOUNT IF EXISTS");statement.execute("CREATE TABLE BANK_ACCOUNT (\n" +"BANK_ID INT,\n" +"BANK_AMOUNT INT,\n" +"PRIMARY KEY(BANK_ID)\n" +");");statement.execute("CREATE TABLE INSURANCE_ACCOUNT (\n" +"INSURANCE_ID INT,\n" +"INSURANCE_AMOUNT INT,\n" +"PRIMARY KEY(INSURANCE_ID)\n" +");");statement.execute("INSERT INTO BANK_ACCOUNT VALUES (1111, 1000);");statement.execute("INSERT INTO INSURANCE_ACCOUNT VALUES (2222, 1000);");statement.close();connection.close();}protected int getBankAmount(int bankId) throws SQLException{Connection connection = dataSource.getConnection();PreparedStatement selectStatement = connection.prepareStatement("SELECT BANK_AMOUNT FROM BANK_ACCOUNT WHERE BANK_ID = ?");selectStatement.setInt(1, bankId);ResultSet resultSet = selectStatement.executeQuery();resultSet.next();int amount = resultSet.getInt(1);resultSet.close();selectStatement.close();connection.close();return amount;}protected int getInsuranceAmount(int insuranceId) throws SQLException{Connection connection = dataSource.getConnection();PreparedStatement selectStatement = connection.prepareStatement("SELECT INSURANCE_AMOUNT FROM INSURANCE_ACCOUNT WHERE INSURANCE_ID = ?");selectStatement.setInt(1, insuranceId);ResultSet resultSet = selectStatement.executeQuery();resultSet.next();int amount = resultSet.getInt(1);resultSet.close();selectStatement.close();connection.close();return amount;}}?
編寫的Junit測試繼承自BankFixture類,測試代碼如下:
package davenkin.step1_failure;import davenkin.BankFixture; import org.junit.Test; import java.sql.SQLException; import static junit.framework.Assert.assertEquals;public class FailureBankServiceTest extends BankFixture {@Testpublic void transferSuccess() throws SQLException{FailureBankDao failureBankDao = new FailureBankDao(dataSource);FailureInsuranceDao failureInsuranceDao = new FailureInsuranceDao(dataSource);FailureBankService bankService = new FailureBankService(dataSource);bankService.setFailureBankDao(failureBankDao);bankService.setFailureInsuranceDao(failureInsuranceDao);bankService.transfer(1111, 2222, 200);assertEquals(800, getBankAmount(1111));assertEquals(1200, getInsuranceAmount(2222));}@Testpublic void transferFailure() throws SQLException{FailureBankDao failureBankDao = new FailureBankDao(dataSource);FailureInsuranceDao failureInsuranceDao = new FailureInsuranceDao(dataSource);FailureBankService bankService = new FailureBankService(dataSource);bankService.setFailureBankDao(failureBankDao);bankService.setFailureInsuranceDao(failureInsuranceDao);int toNonExistId = 3333;bankService.transfer(1111, toNonExistId, 200);assertEquals(1000, getInsuranceAmount(2222));assertEquals(1000, getBankAmount(1111));} }?
運行測試,第一個測試(transferSuccess)成功,第二個測試(transferFailure)失敗。
分析錯誤,原因在于:我們分別從FailureBankService,FailureBankDao和FailureInsuranceDao中調用了三次dataSource.getConnection(),亦即我們創建了三個不同的Connection對象,而Java事務是作用于Connection之上的,所以從在三個地方我們開啟了三個不同的事務,而不是同一個事務。
第一個測試之所以成功,是因為在此過程中沒有任何異常發生。雖然在FailureBankService中將Connection的提交模式改為了手動提交,但是由于兩個DAO使用的是各自的Connection對象,所以兩個DAO中的Connection依然為默認的自動提交模式。
在第二個測試中,我們給出一個不存在的保險賬戶id(toNonExistId),就是為了使程序產生異常,然后在assertion語句中驗證兩張表均沒有任何變化,但是測試在第二個assertion語句處出錯。發生異常時,銀行賬戶中的金額已經減少,而雖然程序發生了rollback,但是調用的是FailureBankService中Connection的rollback,而不是FailureInsuranceDao中Connection的,對保險賬戶的操作根本就沒有執行,所以保險賬戶中依然為1000,而銀行賬戶卻變為了800。
因此,為了使兩個DAO在同一個事務中,我們應該在整個事務處理過程中使用一個Connection對象,在下一篇文章中,我們將講到通過共享Connection對象的方式達到事務處理的目的。
轉載于:https://www.cnblogs.com/fengjian/p/4209651.html
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的Java事务处理全解析(二)——失败的案例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎样提高团队管理能力5
- 下一篇: 显卡内存不足怎么办 解决显卡内存不足问题