トランザクション制御
DAO を DAOFactory から取得して insert を呼べば、レコードは登録されます。
それだけでも動くため、トランザクションを明示しないまま実装が進んでしまうことがあります。
このとき、コミットの単位はプラットフォーム任せになり、二つのテーブルを続けて更新する処理で片方だけが残る余地が生まれます。
im_mirage では、DBを操作する処理を SessionTemplate の中で実行します。
SessionTemplate による境界
SessionTemplate.execute にコールバックを渡すと、その中がトランザクションの範囲になります。
SessionTemplate.execute(new SessionCallback<Void, RuntimeException>() {
@Override
public Void execute(final Session session) {
final OrderDAO dao = DAOFactory.getTenantDatabaseDAO(OrderDAO.class);
dao.insert(order);
return null;
}
});
SessionTemplate が引き受けるのは次の四つです。
- セッションの開始(
begin) - コールバックが正常に終わったときのコミット(
commit) - コールバックが例外を投げたときのロールバック(
rollback) - 処理の成否によらない解放(
release)
解放は finally で必ず実行されます。
コールバック内で発生した例外は、ロールバックのあとそのまま呼び出し元へ伝播します。
ロールバックのためだけに呼び出し側で捕捉して投げ直す必要はありません。
SessionCallback<T, E extends Exception> の型パラメータは、戻り値の型と、コールバックが送出しうる検査例外の型です。
戻り値が不要なら Void を、検査例外を投げないなら RuntimeException を指定します。
値を返す場合は、execute の戻り値をそのまま返します。
public Order findByOrderId(final String orderId) {
return SessionTemplate.execute(new SessionCallback<Order, RuntimeException>() {
@Override
public Order execute(final Session session) {
final OrderDAO dao = DAOFactory.getTenantDatabaseDAO(OrderDAO.class);
final OrderEntity entity = dao.findByOrderId(orderId);
return entity != null ? convertToModel(entity) : null;
}
});
}
参照だけの処理も SessionTemplate で囲むのが標準です。
コネクションの取得と解放の書き方が更新処理と揃い、DAOの取得位置を処理ごとに変えずに済みます。
SessionCallback はラムダ式でも書けます。
型パラメータはコールバックの中身から推論されるため、無名クラスより短く書けます。
public Order findByOrderId(final String orderId) {
return SessionTemplate.execute(s -> {
final OrderDAO dao = DAOFactory.getTenantDatabaseDAO(OrderDAO.class);
final OrderEntity entity = dao.findByOrderId(orderId);
return entity != null ? convertToModel(entity) : null;
});
}
コールバックが送出できる検査例外は一種類です。 種類の異なる検査例外を投げる処理を呼ぶ場合は、コールバックの中で一つの例外に置き換えてから投げます。
ネストしたトランザクション
リポジトリは自分の境界を持ったまま、サービスからも呼ばれます。
このとき、複数のリポジトリを呼ぶサービスの中で execute が入れ子になります。
SessionTemplate.execute(new SessionCallback<Void, RuntimeException>() { // 外側
@Override
public Void execute(final Session session) {
orderRepository.save(order); // 内側で execute が呼ばれる
historyRepository.save(history); // 内側で execute が呼ばれる
return null;
}
});
このとき、内側の execute はコミットもロールバックも行いません。
SessionTemplate はすでにトランザクション中かどうかを判定し、トランザクション中であれば境界の制御を外側へ委ねます。
コミットが起きるのは外側の execute が正常終了したときだけであり、上の例では二つの登録が一つの単位になります。
内側で例外が発生した場合も、ロールバックの判断は外側にあります。 内側の例外を外側で捕捉して処理を続けると、内側の更新は取り消されないまま外側のコミットに含まれます。 入れ子の内側だけを取り消したいという要件があるなら、トランザクションの分割ではなく、処理の順序や更新内容の設計で対応してください。
複数の更新を一つの単位にまとめたいときは、まとめたい範囲を外側の execute で囲みます。
リポジトリ側の execute を消して呼び出し側だけに境界を置く必要はありません。
境界の外で更新しない
// 避けたい実装
final OrderDAO dao = DAOFactory.getTenantDatabaseDAO(OrderDAO.class);
dao.insert(order);
SessionTemplate で囲まないと、コミットの単位が意図した粒度になりません。
どの層が境界を張るかはインフラストラクチャ層の実装ルールで扱っています。
関連ドキュメント
- インフラストラクチャ層の実装ルール:境界を張る層と、サービスの実装
- エンティティと DAO の作成:
DAOFactoryによるDAOの取得 - 2WaySQL:コールバックの中で実行するクエリの書き方
- 全体アーキテクチャ:レイヤの責務と依存の向き