
JSP MVCモデルを使った模擬アプリ課題
以下に、Servlet、JSP、JDBC、フロントコントローラーパターンを活用した3つの課題を提案します。 課題1: 簡易タスク管理アプリケーション 要件 模範 […]
トランザクション管理は、データベースを扱うアプリケーション開発において不可欠な概念です。特に金融システムや在庫管理システムなど、データの整合性が重要なアプリケーションでは、適切なトランザクション管理が必須となります。本記事では、JSP/Servlet環境でのトランザクション管理について、MySQLをメインに詳しく解説します。
トランザクションとは、複数のデータベース操作を1つの論理的な作業単位としてまとめたものです。ACID特性(後述)を保証することで、データの整合性を維持します。
特性 | 説明 |
---|---|
Atomicity(原子性) | トランザクション内の操作はすべて実行されるか、または全く実行されないかのどちらか |
Consistency(一貫性) | トランザクションはデータベースを一貫した状態から別の一貫した状態へ移行させる |
Isolation(分離性) | 並行実行されるトランザクションは互いに干渉しない |
Durability(永続性) | 一度コミットされたトランザクションの結果は永続的に保存される |
[開始]
↓
[アクティブ] ←→ [部分コミット]
↓ ↓
[失敗] [コミット]
↓
[アボート]
setAutoCommit(false)
)commit()
)rollback()
)Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false); // 自動コミットを無効化
// トランザクション内の処理1
try (PreparedStatement stmt1 = conn.prepareStatement("UPDATE accounts SET balance = balance - ? WHERE id = ?")) {
stmt1.setBigDecimal(1, transferAmount);
stmt1.setInt(2, fromAccountId);
stmt1.executeUpdate();
}
// トランザクション内の処理2
try (PreparedStatement stmt2 = conn.prepareStatement("UPDATE accounts SET balance = balance + ? WHERE id = ?")) {
stmt2.setBigDecimal(1, transferAmount);
stmt2.setInt(2, toAccountId);
stmt2.executeUpdate();
}
conn.commit(); // 両方の更新が成功したらコミット
} catch (SQLException e) {
if (conn != null) {
try {
conn.rollback(); // エラーが発生したらロールバック
} catch (SQLException ex) {
ex.printStackTrace();
}
}
throw new RuntimeException("Transfer failed", e);
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true); // 自動コミットを元に戻す
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
分離レベル | ダーティリード | 非再現読み取り | ファントムリード |
---|---|---|---|
READ UNCOMMITTED | 発生する | 発生する | 発生する |
READ COMMITTED | 発生しない | 発生する | 発生する |
REPEATABLE READ (MySQLデフォルト) | 発生しない | 発生しない | 発生する |
SERIALIZABLE | 発生しない | 発生しない | 発生しない |
// 接続ごとに設定
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
// データソースレベルで設定(接続プール)
dataSource.setDefaultTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
DAO層ではなく、サービス層でトランザクションを管理するのがベストプラクティスです。
public class BankService {
private AccountDAO accountDao;
public BankService(AccountDAO accountDao) {
this.accountDao = accountDao;
}
public void transferFunds(int fromAccountId, int toAccountId, BigDecimal amount)
throws InsufficientFundsException, SQLException {
Connection conn = null;
try {
conn = DataSourceManager.getConnection();
conn.setAutoCommit(false);
// DAOに接続を渡す
accountDao.setConnection(conn);
// 残高チェック
BigDecimal fromBalance = accountDao.getBalance(fromAccountId);
if (fromBalance.compareTo(amount) < 0) {
throw new InsufficientFundsException("Insufficient funds in account: " + fromAccountId);
}
// 出金
accountDao.withdraw(fromAccountId, amount);
// 入金
accountDao.deposit(toAccountId, amount);
conn.commit();
} catch (SQLException e) {
if (conn != null) {
conn.rollback();
}
throw e;
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true);
conn.close();
} catch (SQLException e) {
// ログに記録
}
}
// DAOから接続をクリア
accountDao.clearConnection();
}
}
}
複数のサービスメソッドを1つのトランザクションでまとめたい場合に有効です。
@WebFilter("/*")
public class TransactionFilter implements Filter {
private DataSource dataSource;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
try {
Context ctx = new InitialContext();
dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/mydb");
} catch (NamingException e) {
throw new ServletException("Failed to lookup DataSource", e);
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// すべてのDAOがこの接続を使用するように設定
ConnectionHolder.setConnection(conn);
chain.doFilter(request, response);
conn.commit();
} catch (SQLException | RuntimeException e) {
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
// ログに記録
}
}
throw new ServletException("Transaction failed", e);
} finally {
ConnectionHolder.clear();
if (conn != null) {
try {
conn.setAutoCommit(true);
conn.close();
} catch (SQLException e) {
// ログに記録
}
}
}
}
@Override
public void destroy() {}
}
// スレッドローカルで接続を保持するヘルパークラス
public class ConnectionHolder {
private static final ThreadLocal holder = new ThreadLocal<>();
public static void setConnection(Connection conn) {
holder.set(conn);
}
public static Connection getConnection() {
return holder.get();
}
public static void clear() {
holder.remove();
}
}
プログラム的にではなく、アノテーションや設定ファイルでトランザクションを管理する方法です。Spring Frameworkなどでサポートされていますが、ここでは独自実装の例を示します。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Transactional {
int isolation() default Connection.TRANSACTION_READ_COMMITTED;
boolean readOnly() default false;
}
public class TransactionInterceptor {
private DataSource dataSource;
public TransactionInterceptor(DataSource dataSource) {
this.dataSource = dataSource;
}
public Object proceedWithTransaction(Method method, Object target, Object[] args) throws Exception {
Transactional transactional = method.getAnnotation(Transactional.class);
if (transactional == null) {
return method.invoke(target, args);
}
Connection conn = null;
Object result = null;
try {
conn = dataSource.getConnection();
int originalIsolation = conn.getTransactionIsolation();
boolean originalAutoCommit = conn.getAutoCommit();
// トランザクション設定を適用
conn.setTransactionIsolation(transactional.isolation());
conn.setAutoCommit(false);
// DAOに接続を設定
if (target instanceof ConnectionAware) {
((ConnectionAware) target).setConnection(conn);
}
// メソッド実行
result = method.invoke(target, args);
// 読み取り専用でなければコミット
if (!transactional.readOnly()) {
conn.commit();
} else {
conn.rollback(); // 読み取り専用は変更を破棄
}
return result;
} catch (Exception e) {
if (conn != null) {
conn.rollback();
}
throw e;
} finally {
if (conn != null) {
try {
// 元の設定に戻す
conn.setAutoCommit(originalAutoCommit);
conn.setTransactionIsolation(originalIsolation);
conn.close();
} catch (SQLException e) {
// ログに記録
}
}
// DAOから接続をクリア
if (target instanceof ConnectionAware) {
((ConnectionAware) target).clearConnection();
}
}
}
}
public class AccountService {
private AccountDAO accountDao;
public AccountService(AccountDAO accountDao) {
this.accountDao = accountDao;
}
@Transactional
public void transfer(int fromId, int toId, BigDecimal amount) throws InsufficientFundsException {
BigDecimal balance = accountDao.getBalance(fromId);
if (balance.compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
accountDao.withdraw(fromId, amount);
accountDao.deposit(toId, amount);
}
@Transactional(isolation = Connection.TRANSACTION_REPEATABLE_READ, readOnly = true)
public BigDecimal getBalance(int accountId) {
return accountDao.getBalance(accountId);
}
}
伝播動作 | 説明 |
---|---|
REQUIRED | 現在のトランザクションがあれば参加、なければ新規作成 |
REQUIRES_NEW | 常に新規トランザクションを作成 |
NESTED | 現在のトランザクション内にネストされたトランザクションを作成 |
SUPPORTS | 現在のトランザクションがあれば参加、なければトランザクションなし |
NOT_SUPPORTED | トランザクション外で実行、既存のトランザクションは一時停止 |
NEVER | トランザクション外で実行、既存のトランザクションがあれば例外 |
MANDATORY | 既存のトランザクションに参加、なければ例外 |
MySQLのSAVEPOINTを使用して実装します。
public class NestedTransactionExample {
public void outerOperation() throws SQLException {
Connection conn = null;
Savepoint savepoint = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// 外部トランザクションの処理
updateTable1(conn);
try {
savepoint = conn.setSavepoint("INNER_TRANSACTION");
// 内部トランザクションの処理
updateTable2(conn);
updateTable3(conn);
} catch (SQLException innerEx) {
if (savepoint != null) {
conn.rollback(savepoint); // 内部トランザクションのみロールバック
}
// 外部トランザクションは継続
// 必要に応じてエラー処理
}
// 外部トランザクションの続き
updateTable4(conn);
conn.commit();
} catch (SQLException e) {
if (conn != null) {
conn.rollback();
}
throw e;
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true);
conn.close();
} catch (SQLException e) {
// ログに記録
}
}
}
}
private void updateTable1(Connection conn) throws SQLException {
// 更新処理
}
// 他のメソッドも同様...
}
public class TransactionTimeoutExample {
public void executeWithTimeout() throws SQLException {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// タイムアウトを5秒に設定
try (Statement stmt = conn.createStatement()) {
stmt.setQueryTimeout(5); // 秒単位
// 時間がかかるクエリ
stmt.executeUpdate("UPDATE large_table SET column = value WHERE complex_condition");
}
conn.commit();
} catch (SQLTimeoutException e) {
if (conn != null) {
conn.rollback();
}
throw new RuntimeException("Operation timed out", e);
} catch (SQLException e) {
if (conn != null) {
conn.rollback();
}
throw e;
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true);
conn.close();
} catch (SQLException e) {
// ログに記録
}
}
}
}
}
複数のデータベースやリソースにまたがるトランザクションを管理する場合、XAトランザクションを使用します。
public class XATransactionExample {
public void performXATransaction() throws Exception {
// XAデータソースの設定
MysqlXADataSource xaDataSource1 = new MysqlXADataSource();
xaDataSource1.setUrl("jdbc:mysql://db1:3306/mydb");
xaDataSource1.setUser("user");
xaDataSource1.setPassword("password");
MysqlXADataSource xaDataSource2 = new MysqlXADataSource();
xaDataSource2.setUrl("jdbc:mysql://db2:3306/mydb");
xaDataSource2.setUser("user");
xaDataSource2.setPassword("password");
// XAリソースを取得
XAConnection xaConn1 = xaDataSource1.getXAConnection();
XAResource xaRes1 = xaConn1.getXAResource();
Connection conn1 = xaConn1.getConnection();
XAConnection xaConn2 = xaDataSource2.getXAConnection();
XAResource xaRes2 = xaConn2.getXAResource();
Connection conn2 = xaConn2.getConnection();
// グローバルトランザクションIDを生成
byte[] gtrid = "global_transaction_id".getBytes();
int formatId = 1;
try {
// トランザクション開始
Xid xid1 = new MyXid(gtrid, "branch1".getBytes(), formatId);
Xid xid2 = new MyXid(gtrid, "branch2".getBytes(), formatId);
xaRes1.start(xid1, XAResource.TMNOFLAGS);
xaRes2.start(xid2, XAResource.TMNOFLAGS);
// 各データベースで操作実行
try (PreparedStatement stmt1 = conn1.prepareStatement("UPDATE accounts SET balance = balance - ? WHERE id = ?")) {
stmt1.setBigDecimal(1, new BigDecimal("100.00"));
stmt1.setInt(2, 1);
stmt1.executeUpdate();
}
try (PreparedStatement stmt2 = conn2.prepareStatement("UPDATE accounts SET balance = balance + ? WHERE id = ?")) {
stmt2.setBigDecimal(1, new BigDecimal("100.00"));
stmt2.setInt(2, 2);
stmt2.executeUpdate();
}
// フェーズ1: 準備
int prepare1 = xaRes1.end(xid1, XAResource.TMSUCCESS);
int prepare2 = xaRes2.end(xid2, XAResource.TMSUCCESS);
int vote = XAResource.XA_OK;
if (prepare1 == XAResource.XA_RDONLY || prepare2 == XAResource.XA_RDONLY) {
vote = XAResource.XA_RDONLY;
}
// フェーズ2: コミットまたはロールバック
if (vote == XAResource.XA_OK) {
xaRes1.commit(xid1, false);
xaRes2.commit(xid2, false);
} else {
xaRes1.rollback(xid1);
xaRes2.rollback(xid2);
}
} catch (Exception e) {
// エラーが発生したらロールバック
try {
xaRes1.rollback(xid1);
xaRes2.rollback(xid2);
} catch (XAException ex) {
// ログに記録
}
throw e;
} finally {
conn1.close();
conn2.close();
xaConn1.close();
xaConn2.close();
}
}
}
// Xidの実装
class MyXid implements Xid {
protected byte[] gtrid;
protected byte[] bqual;
protected int formatId;
public MyXid(byte[] gtrid, byte[] bqual, int formatId) {
this.gtrid = gtrid;
this.bqual = bqual;
this.formatId = formatId;
}
public byte[] getGlobalTransactionId() { return gtrid; }
public byte[] getBranchQualifier() { return bqual; }
public int getFormatId() { return formatId; }
}
setAutoCommit(false)
を呼び出す必要があるSELECT ... FOR UPDATE
で行ロックを取得READ COMMITTED
を検討addBatch()
とexecuteBatch()
を使用public class BankServiceTest {
private BankService bankService;
private AccountDAO mockAccountDao;
@Before
public void setUp() {
mockAccountDao = Mockito.mock(AccountDAO.class);
bankService = new BankService(mockAccountDao);
}
@Test
public void testTransferSuccess() throws Exception {
// モックの設定
when(mockAccountDao.getBalance(1)).thenReturn(new BigDecimal("200.00"));
// テスト実行
bankService.transfer(1, 2, new BigDecimal("100.00"));
// 検証
verify(mockAccountDao).withdraw(1, new BigDecimal("100.00"));
verify(mockAccountDao).deposit(2, new BigDecimal("100.00"));
}
@Test(expected = InsufficientFundsException.class)
public void testTransferInsufficientFunds() throws Exception {
when(mockAccountDao.getBalance(1)).thenReturn(new BigDecimal("50.00"));
bankService.transfer(1, 2, new BigDecimal("100.00"));
}
}
public class BankServiceIntegrationTest {
private DataSource dataSource;
private BankService bankService;
@Before
public void setUp() throws SQLException {
// テスト用データベースをセットアップ
dataSource = createTestDataSource();
initializeTestDatabase(dataSource);
AccountDAO accountDao = new AccountDAOImpl(dataSource);
bankService = new BankService(accountDao);
}
@Test
public void testTransferWithRollback() throws Exception {
// 初期残高: account1=200, account2=100
BigDecimal initialBalance1 = getBalance(1);
BigDecimal initialBalance2 = getBalance(2);
try {
// 意図的に失敗させる転送 (残高不足)
bankService.transfer(2, 1, new BigDecimal("150.00"));
fail("Expected InsufficientFundsException");
} catch (InsufficientFundsException e) {
// 期待通り
}
// 残高が変わっていないことを確認 (ロールバックされた)
assertEquals(initialBalance1, getBalance(1));
assertEquals(initialBalance2, getBalance(2));
}
private BigDecimal getBalance(int accountId) throws SQLException {
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT balance FROM accounts WHERE id = ?")) {
stmt.setInt(1, accountId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return rs.getBigDecimal("balance");
}
throw new IllegalArgumentException("Account not found: " + accountId);
}
}
}
}
JSP/Servletアプリケーションでのトランザクション管理について、以下のポイントを学びました:
適切なトランザクション管理を実装することで、データの整合性を保ちながら、パフォーマンスの良いアプリケーションを構築できます。実際のプロジェクトでは、これらの基本を理解した上で、Spring Frameworkなどの高度なトランザクション管理機能を活用することが次のステップとなります。