Doma 1.1.0から導入されたローカルトランザクションをどう簡単に使うか?

Domaのローカルトランザクションについては、以下を見てください。

このAPIをどう簡単に呼び出すかについて考えてみました。どれも一長一短があります。どれを使うかは最終的には状況や好みで決まりそうです。

方法1 LocalTransactionを一度だけ取得する方法

まずは一番オーソドックな方法。

    public void testUpdate() throws Exception {
        LocalTransaction tx = AppConfig.getLocalTransaction();
        try {
            tx.begin();

            Employee employee = dao.selectById(1);
            employee.setName("hoge");
            employee.setJobType(JobType.PRESIDENT);
            dao.update(employee);

            tx.commit();
        } finally {
            tx.rollback();
        }
    }

利点はわかりやすいということです。
問題点は、commitとかrollbackの呼び忘れがおきることですね。あと、getLocalTransaction()を実行しただけでトランザクションを開始した気になってbeginを呼び忘れるかもしれません。

方法2 LocalTransactionを毎回取得する方法

次は、AppConfig.getLocalTransaction()で返されるLocalTransactionを変数で受けない方法です。

    public void testUpdate2() throws Exception {
        try {
            AppConfig.getLocalTransaction().begin();

            Employee employee = dao.selectById(1);
            employee.setName("hoge");
            employee.setJobType(JobType.PRESIDENT);
            dao.update(employee);

            AppConfig.getLocalTransaction().commit();
        } finally {
            AppConfig.getLocalTransaction().rollback();
        }
    }

利点としては、ローカル変数を減らせる。beginを呼び忘れが減りそうというのがあります。
commitとかrollbackの呼び忘れがおきるという問題点は残ったままです。AppConfig.getLocalTransaction()が何度かでてくるのでコードが読み難いですね。

方法3 staticインポートを使う方法

コードの読みづらさは、staticインポートで解決できます。

    public void testUpdate3() throws Exception {
        try {
            beginTx();

            Employee employee = dao.selectById(1);
            employee.setName("hoge");
            employee.setJobType(JobType.PRESIDENT);
            dao.update(employee);

            commitTx();
        } finally {
            rollbackTx();
        }
    }

commitとかrollbackの呼び忘れがおきるという問題点は残ったままですが、ずいぶんすっきりした感があります。
ただし、以下のようなクラスを作りstaticインポートする必要があります。

public class LocalTransactionOperations {

    public static void beginTx() {
        AppConfig.getLocalTransaction().begin();
    }

    public static void commitTx() {
        AppConfig.getLocalTransaction().commit();
    }

    public static void rollbackTx() {
        AppConfig.getLocalTransaction().rollback();
    }
}

方法4 無名内部クラスのインスタンス化とメソッド呼び出しを使う方法

commitとかrollbackの呼び忘れがおきるという問題点を解決するにはテンプレートメソッドパターンですね。

    public void testUpdate4() throws Exception {
        new Tx() {
            @Override
            protected void execute() {
                Employee employee = dao.selectById(1);
                employee.setName("hoge");
                employee.setJobType(JobType.PRESIDENT);
                dao.update(employee);
            }
        }.scope();
    }

このようにするとbegin、commit、rollbackを毎回呼び出す必要がなくなります。
次のような抽象クラスをつくっておき、使うほうでこのサブタイプの無名内部クラスを作成し実行します。

public abstract class Tx {

    protected LocalTransaction tx = AppConfig.getLocalTransaction();

    public void scope() {
        try {
            tx.begin();
            execute();
            tx.commit();
        } finally {
            tx.rollback();
        }
    }

    protected abstract void execute();
}

この方法の問題点は、処理を開始するメソッド(ここではscopeメソッド)の呼び出しを忘れがちということですね。
あとコードが若干よみづらくなります。
それと内部クラスは呼び出し元のメソッドのローカル変数がfinalでないとアクセスできないのでその辺の面倒くささというのもありますね。

方法5 無名内部クラスのインスタンス化だけを使う方法

この方法では処理を開始するメソッドをとっぱらってしまいます。

    public void testUpdate5() throws Exception {
        new TxScope() {
            @Override
            protected void execute() {
                Employee employee = dao.selectById(1);
                employee.setName("hoge");
                employee.setJobType(JobType.PRESIDENT);
                dao.update(employee);
            }
        };
    }

無名内部クラスの親となる抽象クラスのコンストラクタでトランザクションのbeginやcommit、rollbackをやってしまうという方法です。コンストラクタの中でいろいろ処理するのは気持ち悪いかもしれませんが。次のようなクラスになります。

public abstract class TxScope {

    protected LocalTransaction tx = AppConfig.getLocalTransaction();
    
    public TxScope() {
        try {
            tx.begin();
            execute();
            tx.commit();
        } finally {
            tx.rollback();
        }
    }

    protected abstract void execute();
}

この方法だと何かを呼び忘れるということはありません。
ただコードが読み難いとか、呼び出し元のメソッドのローカル変数にfinalがついていないとアクセスできないという問題点は残ります。

方法6 コードテンプレート(スニペット)を使う方法

IDEに登録しておき、自動で雛形を出力してしまえば特定のメソッドの呼びだし忘れを防止しやすくなります。この方法はこれ単独で役立つわけではなく他の方法と組み合わせると便利です。
方法3や方法4のテンプレートを登録しておくというのはありだと思います。方法5の場合は、Eclipseだとデフォルトの設定で雛形つくってくれるので、自前で登録する必要はありませんね。

方法7 AOPを使う方法

やっぱり宣言的に実行したいよねという場合は、AOPに対応したライブラリやフレームワーク(DIコンテナなど)を使うことになります。

    @Transactional
    public void execute() {
        Employee employee = dao.selectById(1);
        employee.setName("hoge");
        employee.setJobType(JobType.PRESIDENT);
        dao.update(employee);
    }

利点は、コードがシンプルになること。問題点は、依存ライブラリが増えること、設定が必要なこと、デバッグしくにくくなることなどでしょうか。

追記
  • アプリ全体で、特定の例外のときにはコミットするなどトランザクションのポリシーのようなものを制御するには、方法4から方法7のように共通的にどこかで処理をはさめるタイプがよさげです。
  • ServletFilterとかStrutsのRequestProcessorみたいなところで自動的にトランザクションを制御するなら方法1でいけます。さらに、RequestProcessorでやるならActionに@Transactionalみたいなをつけて方法7のような制御ができますね。
  • 方法4と方法5は戻り値のことが考慮に入っていませんでした。戻り値を考えると方法5は使えないですね。方法4では戻り値の型にジェネリクスを使うべきです。