2011年10月1日土曜日

guiceで jdbc transaction

twitterやfacebookで遊んでいるうちにこのブログも開店休業状態でしたが、かなり久しぶりに投稿します。
guiceを利用を利用している方も多いと思いますが、トランザクションに関しては現在のバージョン3.0ではguice-persist が存在するものの、JPAにしか対応していないという潔さなので困りました。シンプルさを重要視しているみたいなのでJDBCをサポートしてくれるのは少なくとも近くはなさそうです。ネットで探してみたもののよい解決策がみつかりませんでした。

自分でConnectionを操作するよりは出来合いのライブラリを利用したほうが賢いと思い、どうせなら「springframeworkを利用できないか?」ということでguice 3.0 + spring-tx 3.0.6 (DB connection は apache commons-pool) で試してみました。

今回はguiceをメインにするのでspringのDIコンテナはアテにできませんが、springのライブラリのインスタンスをguiceで扱うのは特に問題無いはずです。早速 guiceのconfigureメソッド部分。
DataSourceはTransactionManagerでも利用するのでメンバー変数に設定し、springのPlatformTransactionManagerが登場します。
  protected void configure() {
    bind(UserDao.class).to(JdbcUserDao.class);
    bind(DataSource.class).toInstance(dataSource);
    bind(PlatformTransactionManager.class).toInstance(newTransactionManager());
    bindInterceptor(any(), 
      annotatedWith(Transactional.class), getTransactionInterceptor());
  }
次にDataSource生成部分。上記のconfigurecommons-poolでコネクションプールをします。
  private DataSource newDataSource() {
    BasicDataSource dataSource = new BasicDataSource();
    dataSource.setUrl("jdbc.url");
    dataSource.setUsername("database.user");
    dataSource.setPassword("database.password");
    dataSource.setDefaultAutoCommit(false);
    dataSource.setMaxActive(20);
    dataSource.setMaxWait(5);
    return dataSource;
  }
さらにTransactionManager生成の処理。springのDataSourceTransactionManagerを利用してます。
  private DataSourceTransactionManager newTransactionManager() {
    DataSourceTransactionManager manager = 
      new DataSourceTransactionManager(dataSource);
    return manager;
  }
重要なInterceptor処理ですが、その前に@Transactionalアノテーションはguice, spring それぞれ独自のものを持っていてコンテナ内で利用されるべきものなので、これだけは自作のTransactionアノテーションを作成しました。インターセプト後も自作のメソッドで実装するのでマーカーアノテーションにしています。(Propagationやrollbackの例外条件などはあまり気にしなくても現時点では大丈夫なので)。
  @Retention(RetentionPolicy.RUNTIME)
  @Target({ElementType.METHOD})
  @Inherited
  public @interface Transactional {

  }
最後に重要なInterceptor実装です。トランザクションの対象となるメソッドの前後に設定してあげるだけの実装で、RuntimeException時にrollbackするようにしています。またその他のオプションは txManager.getTransaction(null) の呼び出し時の引数で決定するので springの実装 (AbstractPlatformTransactionManager#getTransaction) を見ながら必要に応じて自作のTransactionアノテーションとこのInterceptor実装を修正していけば可能になります。
public static class TransactionInterceptor implements MethodInterceptor {
  private static final Logger logger = 
    LoggerFactory.getLogger(TransactionInterceptor.class);

  public Object invoke(MethodInvocation invocation) throws Throwable {
    Object obj = null;
    PlatformTransactionManager txManager = 
      injector.getInstance(PlatformTransactionManager.class);
    TransactionStatus status = null;
    try {
      status = txManager.getTransaction(null);

      obj = invocation.proceed();
  
      txManager.commit(status);
    } catch (RuntimeException e) {
      logger.warn("database rollback: {}", e);
      txManager.rollback(status);
    }
    return obj;
  }
}

これで無事にトランザクションの実現ができました。