Micronaut + Doma2 連携(超簡易版)

f:id:nemuzuka:20200611200142p:plain

やっぱり RDBMS と連携したかったのでやってみました。

参考にしたのは

です。

build.gradle

plugins {
  ...
  id 'org.seasar.doma.compile' version '1.1.0'
}

dependencies {
  ...
  annotationProcessor "org.seasar.doma:doma-processor:2.37.0"
  implementation "org.seasar.doma:doma-core:2.37.0"
  implementation 'io.micronaut.flyway:micronaut-flyway'

  runtimeOnly 'io.micronaut.sql:micronaut-jdbc-hikari'
  runtimeOnly "com.h2database:h2:1.4.200"
  ...
}

Doma2 はアノテーションプロセッサで interface から Dao の実装クラスを自動的に作ってくれます。

コネクションプールには HikariCP を使い、データベースのマイグレーションツールとして flyway を使います。 RDBMS は H2 にしましたが、この辺りは適宜変えてください。

LocalTransactionManager 関連

ここ に書いてあるように、Config の実装クラスを定義して、@Singleton で登録します。

DomaConfigFactory

@Factory
public class DomaConfigFactory {

  @Singleton
  public LocalTransactionDataSource localTransactionDataSource(DataSource dataSource) {
    return new LocalTransactionDataSource(dataSource);
  }

  @Singleton
  public LocalTransactionManager localTransactionManager(LocalTransactionDataSource dataSource) {
    return new LocalTransactionManager(
        dataSource.getLocalTransaction(ConfigSupport.defaultJdbcLogger));
  }
}

LocalTransactionManager を @Factory 経由で登録します。

DomaConfig

@Singleton
@RequiredArgsConstructor
public class DomaConfig implements Config {

  private final LocalTransactionDataSource dataSource;

  private final LocalTransactionManager transactionManager; // 1

  @Override
  public DataSource getDataSource() {
    return dataSource;
  }

  @Override
  public Dialect getDialect() {
    return new H2Dialect();  // 2
  }

  @Override
  public TransactionManager getTransactionManager() {
    return transactionManager;
  }
}
  1. LocalTransactionManager は Factory で登録したインスタンスを Inject します
  2. プロパティからとるとか良い感じにしてください。今回は決め打ちにしました

Dao

@DaoConfig

@AnnotateWith(annotations = @Annotation(target = AnnotationTarget.CLASS, type = Singleton.class))
public @interface DaoConfig {}

TaskDao

@Dao
@DaoConfig // 1
public interface TaskDao {

  @Insert
  Result<TaskEntity> insert(TaskEntity taskEntity);

  @Select
  @Sql("SELECT * FROM tasks ORDER BY task_id")
  List<TaskEntity> selectAll();
}
  1. Dao の interface に @DaoConfig を付与することで、アノテーションプロセッサで生成する impl クラスに @Singleton を付与してくれます。

後は、いつものように Doma2 を使えば良いです。

接続情報

RDBMS と flyway 用の設定をします。

datasources:  #1
  default:
    url: 'jdbc:h2:mem:micronaut-sample;LOCK_TIMEOUT=10000;MODE=PostgreSQL'
    username: 'sa'
    password: ''
    driverClassName: 'org.h2.Driver'
    schema-generate: CREATE_DROP
    dialect: H2
flyway:
  datasources:
    default:
      locations: db/migration #2
  1. 接続情報を設定します。schema-generate はテストの時だけ設定します。
  2. flyway が読み込む sql ファイルの配置場所を指定します。

Dao のテスト

TaskDaoTest

@MicronautTest // 1
@Property(name = "flyway.datasources.default.locations", value = "db/migration,db_fixtures/minimum") // 2
class TaskDaoTest {
  @Inject private TaskDao sut; // 3

  @Inject private LocalTransactionManager transactionManager; // 4

  @Test
  void testInsert() {
    transactionManager.required( // 5
        () -> {
          // setup
          var task = new TaskEntity(UUID.randomUUID().toString(), "タスク名", "内容");

          // exercise
          var actual = sut.insert(task);

          // verify
          assertThat(actual.getCount()).isEqualTo(1);
          assertThat(actual.getEntity()).isEqualTo(task);
          assertThat(sut.selectAll()).hasSize(3);
        });
  }

  @Test
  void testSelectAll() {
    transactionManager.required(
        () -> {
          // exercise
          var actual = sut.selectAll();

          // verify
          assertThat(actual).hasSize(2);
          assertThat(actual.get(0).getTaskId()).isEqualTo("001-dummy");
          assertThat(actual.get(1).getTaskId()).isEqualTo("002-dummy");
        });
  }
}
  1. @MicronautTest をつけることで、Inject できるようにします
  2. テスト開始時に flyway で table 作成するだけでなく、fixture となるデータを sql で入れたかったので、プロパティ flyway.datasources.default.locations を上書きます
    • db_fixtures/minimum は test/resources ディレクトリに配置しています。
  3. Dao を Inject することでアノテーションプロセッサで生成した Dao の実装クラスが Inject されます。
  4. LocalTransactionManager を Inject します
  5. Inject した LocalTransactionManager を使用してテストします。Dao のテストはトランザクションをテストクラスで制御しています。参考

トランザクション

@Transactional

アノテーショントランザクションを制御できるようにします。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Around
@Type(TransactionInterceptor.class)  // 1
public @interface Transactional {}
  1. @Transactional がついている時はトランザクションを張るようにします

TransactionInterceptor

@Singleton
@RequiredArgsConstructor
public class TransactionInterceptor implements MethodInterceptor<Object, Object> {

  private final LocalTransactionManager transactionManager;

  @Override
  public Object intercept(MethodInvocationContext<Object, Object> context) {
    return transactionManager.required((Supplier<Object>) context::proceed); // 1
  }
}
  1. 今回は手を抜いて、propagation は REQUIRED だけにしました

TaskUseCase

@Singleton
@RequiredArgsConstructor
@Transactional  // 1
public class TaskUseCase {

  private final TaskRepository taskRepo;

  ...
}
  1. 今回は UseCase に @Transactional を付与しました
    • このメソッド呼び出しが正常終了すると commit し、Exception を throw すると rollback します

まとめ

micronaut で doma2 を使ってアクセスできるようになりました。 Micronaut Data に Doma2 を...とも思ったのですが、簡単にいかなそうなので小手先でできるようにしてみました。Java の ORM 何が流行ってるのかわからん。

若干やっつけ感はありますが、アノテーショントランザクション管理できるようになりました。 今回の差分は大体こんな感じです。

*1:超感謝です!