(その14)トランザクション管理

AOP用途の73%を占めるトランザクション管理*1。これが分かればとりあえずGuiceを使ってみようという人が増えるかもしれないので、ちょっと試してみます。

トランザクション機能は他ライブラリを使う

Guiceトランザクションに関する機能を一切持っていません。ドキュメントを見ると"@Transactional"とか書いてあって紛らわしいのですが、「そういうアノテーションを使えば?」という程度の話です。なので、実際にトランザクション管理を行う場合は、自力で実装するか、他のライブラリの機能を使う事になります。今回はSpringのTransactionInterceptorを使ってみますが、Sesar2でもたぶん同じような事が出来ますし、ちょっとインターセプタを書けばHibernateトランザクション機能を使う事も出来るはずです。

今回の要件

他にもタイムアウトとか分離レベルなど色々あるとは思いますが、今回のサンプルに影響が無いと思われるので省略します。

SpringのTransactionInterceptorを使ってみる

という事で試してみます。まずDBにアクセスする適当なDao

public class DaoImpl implements Dao {
  @Inject
  private SimpleJdbcTemplate jt;
  
  public void setupTable() {
    jt.update("drop table if exists ID_VALUE");
    jt.update("create table ID_VALUE(ID int not null auto_increment,VALUE varchar(128) not null, primary key(ID)) engine=InnoDB");
  }
  
  public int insert(String value) {
    return jt.update("insert into ID_VALUE(VALUE) values (?)", value);
  }
}

Spring2.0で加わったSimpleJdbcTemplateを使用していますが、この処理では全然有難みが無いですね。DBはMySQLInnoDBを使います。
次にこのDaoを呼び出すサービスの実装。例外を渡すと投げるところはkoichikさんのSpring入門記を真似ました。

import org.springframework.transaction.annotation.Transactional;

public class ServiceImpl implements Service {
  @Inject
  private Dao dao;
  
  public void setupTable() {
    dao.setupTable();
  }
  
  public void insert(String value, RuntimeException ex) {
    dao.insert(value);
    if(ex != null) throw ex;
  }
  
  @Transactional
  public void transactionalInsert(String value, RuntimeException ex) {
    dao.insert(value);
    if(ex != null) throw ex;
  }
}

いよいよ@Transactionalが出てきましたが、これはSpringのアノテーションをそのまま使っています。アノテーションGuiceAOP指定でannotateWith(Transactional.class)と指定する為の単なるマーカなので、自作した@Hogeでも何でも良いのですが、せっかくなので使っています。
そしてサービスを呼び出すコードです。ちゃんとトランザクションになっている事が分かるように、トランザクション無しの例外有り無し、トランザクション有りの例外有り無し、の4パターンを2回繰り返しています。IllegalAccessErrorはちゃんと例外が投げられている事の確認目的です。

public class Client {
  @Inject
  private Service service;
  
  public void execute() {
    service.setupTable();
    for(int i = 0; i < 2; i++) {
      // トランザクション無しinsert
      service.insert("non transactional insert", null);
      try {
        // トランザクション無しinsert。例外でもロールバックしないはず
        service.insert("non transactional insert with RuntimeException", new RuntimeException());
        throw new IllegalAccessError();
      } catch(RuntimeException ex) {
      }
      
      // トランザクション有りinsert
      service.transactionalInsert("transactional insert", null);
      try {
        // トランザクション有りinsert。例外によってロールバックするはず
        service.transactionalInsert("transactional insert with RuntimeException", new RuntimeException());
        throw new IllegalAccessError();
      } catch(RuntimeException ex) {
      }
    }
  }
}

そして最後に、GuiceのDIコンテナを設定して起動するクラス。

public class Boot {
  public static void main(String[] args) {
    Injector injector = Guice.createInjector(new AbstractModule() {
      protected void configure() {
        // @Transcationalがつけられたメソッドにインターセプタを設定
        TransactionInterceptor transactionInterceptor = createTransactionInterceptor();
        bindInterceptor(any(), annotatedWith(Transactional.class), transactionInterceptor);
        
        bind(SimpleJdbcTemplate.class).toProvider(SimpleJdbcTemplateProvider.class);
        bind(Service.class).to(ServiceImpl.class);
        bind(Dao.class).to(DaoImpl.class);
      }

      // インターセプタの生成
      private TransactionInterceptor createTransactionInterceptor() {
        // DataSource
        DataSource ds = new DriverManagerDataSource(
            "com.mysql.jdbc.Driver", "jdbc:mysql://127.0.0.1:3306/GUICE", "guice", "guice");
        bind(DataSource.class).toInstance(ds);
        
        // TransactionManager
        PlatformTransactionManager txManager = new DataSourceTransactionManager(ds);
        
        // TransactionInterceptor
        Properties props = new Properties();
        // TransactionInterceptorがメソッド名を判定するので、*を指定しておく。
        props.setProperty("*", "PROPAGATION_REQUIRED");
        TransactionInterceptor transactionInterceptor = new TransactionInterceptor(txManager, props);
        return transactionInterceptor;
      }
    });
    // 実行
    injector.getInstance(Client.class).execute();
  }
}

その3で試した通り、AOPはbindInterceptor(適用クラス, 適用メソッド, インターセプタ)でweaveします。このサンプルでは、適用するメソッドとしてTransactionalというアノテーションがついたものを指定しています。
ところでご覧の通りインターセプタを生成する部分が非常にアレです。普通に考えるとDataSourceやTransactionManagerのProviderを作ってInterceptorProviderにinjectしたいところなのですが、インターセプタを設定するbindInterceptor()の第3引数はMethodInterceptorしか取れないので、その前にインスタンスを作っておかなければならないようです。もう1つ事前にinjectorを作るとかすれば多少は良くなりそうですが、どうもいまいちな感じ。私の勘違いなのか実装上不可能なのか、何なのでしょうか。もしエレガントに設定する方法をご存知の方がいらっしゃったら、是非ご教示下さい。
では実行してみます。以下にデバッグログを抜粋しました。

// インターセプタが作られた
DEBUG [org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource] - Adding transactional method [*] with attribute [PROPAGATION_REQUIRED,ISOLATION_DEFAULT]
// 1 : トランザクション無しinsert
DEBUG [org.springframework.jdbc.core.JdbcTemplate] - Executing prepared SQL statement [insert into ID_VALUE(VALUE) values (?)]
DEBUG [org.springframework.jdbc.core.JdbcTemplate] - SQL update affected 1 rows
// 2 : トランザクション無しinsert
DEBUG [org.springframework.jdbc.core.JdbcTemplate] - Executing prepared SQL statement [insert into ID_VALUE(VALUE) values (?)]
DEBUG [org.springframework.jdbc.core.JdbcTemplate] - SQL update affected 1 rows
// 3 : トランザクション有りinsert
DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Creating new transaction with name [sample.guice.transaction.service.ServiceImpl.transactionalInsert]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG [org.springframework.jdbc.core.JdbcTemplate] - Executing prepared SQL statement [insert into ID_VALUE(VALUE) values (?)]
DEBUG [org.springframework.jdbc.core.JdbcTemplate] - SQL update affected 1 rows
// コミットされてる
DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Initiating transaction commit
DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Committing JDBC transaction on Connection [com.mysql.jdbc.Connection@ef5502]
// 4 : トランザクション有りinsert
DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Creating new transaction with name [sample.guice.transaction.service.ServiceImpl.transactionalInsert]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG [org.springframework.jdbc.core.JdbcTemplate] - Executing prepared SQL statement [insert into ID_VALUE(VALUE) values (?)]
// ロールバックされてる
DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Initiating transaction rollback
DEBUG [org.springframework.jdbc.datasource.DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [com.mysql.jdbc.Connection@111a775]

上手くいってるようです(ほんとは何回かはまったのですが、最後には上手くいったという事で)。
DBの中身は、

mysql> select * from ID_VALUE;
+----+------------------------------------------------+
| ID | VALUE                                          |
+----+------------------------------------------------+
|  1 | non transactional insert                       |
|  2 | non transactional insert with RuntimeException |
|  3 | transactional insert                           |
|  5 | non transactional insert                       |
|  6 | non transactional insert with RuntimeException |
|  7 | transactional insert                           |
+----+------------------------------------------------+
6 rows in set (0.00 sec)

ちゃんと、トランザクション有りで例外が発生したケースだけロールバックされてレコードが無いですね。
という訳で、Guiceでシンプルなトランザクション管理を行うケースを見てみました。インターセプタ生成部分に課題が残っていますが、そこは定型的なコードなので気にしないとすれば、メソッドに@TransactionalをつけてbindInterceptor()するだけで処理をトランザクション化する事ができるようになる事が分かりました。

追記(2008/3/5)

約1年振りにコメントを頂きました。tarouさんありがとうございます。
という事で、SimpleJdbcTemplateProviderのソースを貼り付けときます。

public class SimpleJdbcTemplateProvider implements Provider<SimpleJdbcTemplate> {

  private SimpleJdbcTemplate jt;
  
  @Inject
  public void setDataSource(DataSource dataSource) {
    jt = new SimpleJdbcTemplate(dataSource);
  }
  
  public SimpleJdbcTemplate get() {
    return jt;
  }
}

*1:数値は勘