在本系列的上一篇文章中,我們講到了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();
@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對象的方式達到事務處理的目的。