Java事務之二——失敗的案例


  在本系列的上一篇文章中,我們講到了Java事務處理的基本問題,並且講到了Service層和DAO層,在本篇文章中,我們將以BankService為例學習一個事務處理失敗的案例。

  請通過以下方式下載github源代碼:

git clone https://github.com/davenkin/java_transaction_workshop.git

 

  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(); @Before public 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 { @Test public 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)); } @Test public 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對象的方式達到事務處理的目的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM