CLOVER🍀

That was when it all began.

QuarkusのHibernate Reactive with Panache Extensionを試してみようとした話

これは、なにをしたくて書いたもの?

Quarkusに、Hibernate Reactive with Panacheというものに関するドキュメントがあるのが前々から気になっていたので。

Quarkus - Simplified Hibernate Reactive with Panache

今回、1度試してみることにしました。

結果だけ言うと、また時間が経ってから見てみようかな、という気分にはなりましたが。とりあえず、情報はいろいろ見てみたので
そのあたりを書いていこうと思います。

Hibernate Reactive

まずは、Hibernate Reactiveについて。

The reactive API for Hibernate ORM. - Hibernate Reactive

Hibernate Reactiveは、名前のとおりHibernate ORMのリアクティブ版といった感じのものです。

データベースのドライバーとしては、JDBCではなくVert.xのリアクティブデータベースクライアントを使用します。

Vert.x / Databases

Hibernate Reactiveのドキュメントはこちら。

Hibernate Reactive 1.1.2.Final Reference Documentation

現時点でバージョン1.0に到達していて、基本的な情報はHibernate ORMやJPAのドキュメントを参照すること、となっています。
JPAのエンティティ用のアノテーションが使えたりもするので、Hibernate ORMと同等のことができるのかと思いきやそうでもなかったりします。

たとえば、現時点でHibernate ReactiveはJPAのトランザクション管理との統合や、複数のデータソースを扱うことはできません。
JTAにも対応していないので、@Transactionalアノテーションによるトランザクション管理はできないということですね。

However, notice that the transaction is a resource local transaction only, delegated to the underlying Vert.x database client, and does not span multiple datasources, nor integrate with JPA container-managed transactions.

Hibernate Reactive does not currently support distributed (XA) transactions.

Hibernate Reactive Reference Documentation / Transactions

Quarkus Hibernate Reactive with Panache Extension

次に、QuarkusのHibernate Reactive with Panache Extensionについて。

Quarkus - Simplified Hibernate Reactive with Panache

Hibernate ORM Extensionには、Hibernate ORMとPanacheそれぞれドキュメントがあるのですが、Hibernate Reactiveの方には
Panacheと合わせたものしかありません。

Quarkus - Using Hibernate ORM and JPA

Quarkus - Simplified Hibernate ORM with Panache

Panacheというのは、QuarkusでHibernateを簡単に使えるようにするためのExtensionです。

QuarkusでHibernate ORMを使う時点で、persistence.xmlを書かなくても良いなど、使いやすくはなっているのですが。
Panacheを使うことで、以下のことができるようになります。

  • JPAのエンティティクラスをPanacheEntityクラス(またはPanacheEntityBaseクラス)のサブクラスとすることによる、Active Record Patternの利用
  • PanacheRepositoryインターフェース(またはPanacheRepositoryBaseインターフェース)を実装したリポジトリクラスを作成することによる、Repository Patternの利用
  • モックのサポート

パターンについては、どちらの方法を選んでも便利なクエリ用のメソッドが追加されていたり、JPQL全体を書かなくても簡易にJPQL相当のものが   書けたり、ページングができたりとHibernate ORMが簡単に使えるようになります。

Hibernate Reactive with Panacheも基本的には同じ話なのですが、Active RecordとRepositoryの各パターンとモックに加えて、以下のことが
できるようになります。

Simplified Hibernate Reactive with Panache / Transactions

ただ、モックについてはRepository Patternのみのサポートになります。

JTAの@Transactionalでは、Hibernate Reactiveのトランザクション管理はできません。ドキュメントを読んでいて、これはどうしてだろう?と
思っていたのですが、前述のとおりそもそもHibernate ReactiveがJTAのトランザクションに対応していないからです。

Hibernate Reactiveでのトランザクション管理は、以下のイメージになります。

session.withTransaction( tx -> session.persist(book) )

@ReactiveTransactionalアノテーションを付与すると、これをインターセプターで行うことになります。

注意点として、@ReactiveTransactionalアノテーションを付与したメソッドはUniを返すことになるのですが、トランザクション操作は
このUni内で使用するオペレーター内にいなければならないということですね。

quarkus/ReactiveTransactionalInterceptorBase.java at 2.6.3.Final · quarkusio/quarkus · GitHub

メソッドの戻り値となるUni以外のところにある更新処理は、トランザクションの範囲外になります。

ところでこの@ReactiveTransactionalアノテーション、Hibernate Reactive ExtensionではなくHibernate Reactive with Panacheの方にあるのは
どうしてなんでしょうね。

ドキュメントに記載はないものの、Hibernate Reactive Extensionも存在はするようですが。

f:id:Kazuhira:20220126013800p:plain

よく見ると、両方ともPreviewですね…code.quarkus.ioを見て気づきました…。

テストでのトランザクションに関しては、JTAであれば@TestTransactionというアノテーションがあるようですが、
Hibernate Reactive with Panacheには対応するものがドキュメントにはなさそうです。

このあたりがそれっぽいかな、と思いますが、ドキュメントには書かれていないのでいったんメモのみにします。

https://github.com/quarkusio/quarkus/blob/2.6.3.Final/test-framework/common/src/main/java/io/quarkus/test/TestReactiveTransaction.java

https://github.com/quarkusio/quarkus/tree/2.6.3.Final/test-framework/junit5-vertx

そしてもうひとつ。現時点で、Hibernate ReactiveはFlyway…というかJDBCを使うExtensionと共存できません。

たとえば、以下のようにFlywayを追加してQuarkusを起動しようとすると

    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-jdbc-mysql</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-flyway</artifactId>
    </dependency>

起動に失敗します。

Caused by: java.lang.RuntimeException: javax.persistence.PersistenceException: Unable to build EntityManagerFactory
        at io.quarkus.hibernate.orm.runtime.JPAConfig.startAll(JPAConfig.java:72)
        at io.quarkus.hibernate.orm.runtime.JPAConfig_Subclass.startAll$$superforward1(Unknown Source)
        at io.quarkus.hibernate.orm.runtime.JPAConfig_Subclass$$function$$5.apply(Unknown Source)
        at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:54)
        at io.quarkus.arc.runtime.devconsole.InvocationInterceptor.proceed(InvocationInterceptor.java:62)
        at io.quarkus.arc.runtime.devconsole.InvocationInterceptor.monitor(InvocationInterceptor.java:51)
        at io.quarkus.arc.runtime.devconsole.InvocationInterceptor_Bean.intercept(Unknown Source)
        at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:41)
        at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:41)
        at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:32)
        at io.quarkus.hibernate.orm.runtime.JPAConfig_Subclass.startAll(Unknown Source)
        at io.quarkus.hibernate.orm.runtime.HibernateOrmRecorder.startAllPersistenceUnits(HibernateOrmRecorder.java:97)
        at io.quarkus.deployment.steps.HibernateOrmProcessor$startPersistenceUnits1868654632.deploy_0(Unknown Source)
        at io.quarkus.deployment.steps.HibernateOrmProcessor$startPersistenceUnits1868654632.deploy(Unknown Source)
        ... 13 more
Caused by: javax.persistence.PersistenceException: Unable to build EntityManagerFactory
        at io.quarkus.hibernate.reactive.runtime.FastBootHibernateReactivePersistenceProvider.createEntityManagerFactory(FastBootHibernateReactivePersistenceProvider.java:93)
        at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:80)
        at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:55)
        at io.quarkus.hibernate.orm.runtime.JPAConfig$LazyPersistenceUnit.get(JPAConfig.java:149)
        at io.quarkus.hibernate.orm.runtime.JPAConfig$1.run(JPAConfig.java:58)
        ... 1 more
Caused by: java.lang.IllegalStateException: Booting an Hibernate Reactive serviceregistry on a non-reactive RecordedState!
        at io.quarkus.hibernate.reactive.runtime.boot.registry.PreconfiguredReactiveServiceRegistryBuilder.checkIsReactive(PreconfiguredReactiveServiceRegistryBuilder.java:77)
        at io.quarkus.hibernate.reactive.runtime.boot.registry.PreconfiguredReactiveServiceRegistryBuilder.<init>(PreconfiguredReactiveServiceRegistryBuilder.java:67)
        at io.quarkus.hibernate.reactive.runtime.FastBootHibernateReactivePersistenceProvider.rewireMetadataAndExtractServiceRegistry(FastBootHibernateReactivePersistenceProvider.java:177)
        at io.quarkus.hibernate.reactive.runtime.FastBootHibernateReactivePersistenceProvider.getEntityManagerFactoryBuilderOrNull(FastBootHibernateReactivePersistenceProvider.java:156)
        at io.quarkus.hibernate.reactive.runtime.FastBootHibernateReactivePersistenceProvider.createEntityManagerFactory(FastBootHibernateReactivePersistenceProvider.java:82)
        ... 5 more

これは、JDBCを使ったデータソースが存在するとHibernate Reactiveが検出してしまうからのようです。

Hibernate reactive + Flyway extension causes UnsatisfiedResolutionException · Issue #10716 · quarkusio/quarkus · GitHub

個人的には、これくらいの情報がまとめられたら今回はもういいかな、と思ったりもするのですが。ここから先は、あまり見る意味は
ありません。

一応この後に、試したことを少し書いておきましょう。また時間が経ってHibernate Reactiveが進化したら、また見てみようかなと思います。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.1 2021-10-19
OpenJDK Runtime Environment (build 17.0.1+12-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.1+12-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.4 (9b656c72d54e5bacbed989b64718c159fe39b537)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.1, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-96-generic", arch: "amd64", family: "unix"

データベースにはMySQLを使用し、172.17.0.2で動作しているものとします。

$ mysql --version
mysql  Ver 8.0.28 for Linux on x86_64 (MySQL Community Server - GPL)

アプリケーションを作成する

まずは、Quarkusプロジェクトを作成します。

$ mvn io.quarkus.platform:quarkus-maven-plugin:2.6.3.Final:create \
    -DprojectGroupId=org.littlewings \
    -DprojectArtifactId=hibernate-reactive \
    -DprojectVersion=0.0.1-SNAPSHOT \
    -Dextensions="resteasy-reactive,hibernate-reactive-panache,reactive-mysql-client"

選択されたExtension。

[INFO] Looking for the newly published extensions in registry.quarkus.io
[INFO] -----------
[INFO] selected extensions:
- io.quarkus:quarkus-resteasy-reactive
- io.quarkus:quarkus-reactive-mysql-client
- io.quarkus:quarkus-hibernate-reactive-panache

[INFO]
applying codestarts...
[INFO] 📚  java
🔨  maven
📦  quarkus
📝  config-properties
🔧  dockerfiles
🔧  maven-wrapper
🚀  resteasy-reactive-codestart

プロジェクト内に移動します。

$ cd hibernate-reactive

Maven依存関係は、このような感じに。

  <dependencies>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy-reactive</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-reactive-mysql-client</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-hibernate-reactive-panache</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-arc</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-junit5</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>io.rest-assured</groupId>
      <artifactId>rest-assured</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

quarkus-hibernate-reactive-panacheとquarkus-reactive-mysql-clientの2つが、今回少なくとも必要なものです。
quarkus-resteasy-reactiveもいますが、今回は使うのをやめました。

自動生成されたソースコードは削除。

$ rm src/main/java/org/littlewings/* src/test/java/org/littlewings/*

アクセスするテーブルのお題は、書籍とします。

create table book (
  isbn varchar(14),
  title varchar(255),
  price int,
  primary key(isbn)
);

JPAのエンティティクラス。こちらは、Panacheに関するクラスを使わないようにしました。

src/main/java/org/littlewings/quarkus/hibernate/reactive/Book.java

package org.littlewings.quarkus.hibernate.reactive;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "book")
public class Book {
    @Id
    String isbn;

    @Column
    String title;

    @Column
    Integer price;

    public static Book create(String isbn, String title, Integer price) {
        Book book = new Book();
        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);

        return book;
    }

    // getter/setterは省略。
}

Panacheは、Repository Patternとして使うことにします。

src/main/java/org/littlewings/quarkus/hibernate/reactive/BookRepository.java

package org.littlewings.quarkus.hibernate.reactive;

import java.util.List;
import javax.enterprise.context.ApplicationScoped;

import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import io.quarkus.panache.common.Parameters;
import io.smallrye.mutiny.Uni;

@ApplicationScoped
public class BookRepository implements PanacheRepositoryBase<Book, String> {
    public Uni<Book> findByIsbn(String isbn) {
        return findById(isbn);
    }

    public Uni<List<Book>> findByPrice(Integer price) {
        return list(
                "price > :price order by price desc",
                Parameters.with("price", price)
        );
    }
}

ドキュメントによると、Repository Patternを使う場合はPanacheRepositoryインターフェースを実装するのが基本のようですが、
PanacheRepositoryインターフェースを使用した場合はエンティティクラスの主キー(@Id)がLongであることが前提になります。

Simplified Hibernate Reactive with Panache / Solution 2: using the repository pattern

主キーを他の型で扱う場合は、PanacheRepositoryBaseインターフェースを実装したクラスを作成します。

Simplified Hibernate Reactive with Panache / / Custom IDs

今回は、こちらのパターンを使用しました。この考え方は、Active Record Patternを使っても同様です。

そして、Panacheが提供するインターフェース(Active Record Patternの場合はクラス)を使用することで、便利なメソッドが使えるように
なります。

Simplified Hibernate Reactive with Panache / Most useful operations

これらの定義も、PanacheRepositoryBaseインターフェースが提供するデフォルト実装を利用しています。

    public Uni<Book> findByIsbn(String isbn) {
        return findById(isbn);
    }

    public Uni<List<Book>> findByPrice(Integer price) {
        return list(
                "price > :price order by price desc",
                Parameters.with("price", price)
        );
    }

どのようなメソッドが定義されているかというと、PanacheRepositoryBaseインターフェースのソースコードを見ればわかるのですが

https://github.com/quarkusio/quarkus/blob/2.6.3.Final/extensions/panache/hibernate-reactive-panache/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/PanacheRepositoryBase.java

デフォルトメソッドを書き出すと、こんな感じですね。

$ curl -s https://raw.githubusercontent.com/quarkusio/quarkus/2.6.3.Final/extensions/panache/hibernate-reactive-panache/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/PanacheRepositoryBase.java | grep 'public default'
    public default Uni<Mutiny.Session> getSession() {
    public default Uni<Entity> persist(Entity entity) {
    public default Uni<Entity> persistAndFlush(Entity entity) {
    public default Uni<Void> delete(Entity entity) {
    public default boolean isPersistent(Entity entity) {
    public default Uni<Void> flush() {
    public default Uni<Entity> findById(Id id) {
    public default Uni<Entity> findById(Id id, LockModeType lockModeType) {
    public default PanacheQuery<Entity> find(String query, Object... params) {
    public default PanacheQuery<Entity> find(String query, Sort sort, Object... params) {
    public default PanacheQuery<Entity> find(String query, Map<String, Object> params) {
    public default PanacheQuery<Entity> find(String query, Sort sort, Map<String, Object> params) {
    public default PanacheQuery<Entity> find(String query, Parameters params) {
    public default PanacheQuery<Entity> find(String query, Sort sort, Parameters params) {
    public default PanacheQuery<Entity> findAll() {
    public default PanacheQuery<Entity> findAll(Sort sort) {
    public default Uni<List<Entity>> list(String query, Object... params) {
    public default Uni<List<Entity>> list(String query, Sort sort, Object... params) {
    public default Uni<List<Entity>> list(String query, Map<String, Object> params) {
    public default Uni<List<Entity>> list(String query, Sort sort, Map<String, Object> params) {
    public default Uni<List<Entity>> list(String query, Parameters params) {
    public default Uni<List<Entity>> list(String query, Sort sort, Parameters params) {
    public default Uni<List<Entity>> listAll() {
    public default Uni<List<Entity>> listAll(Sort sort) {
    public default Multi<Entity> stream(String query, Object... params) {
    public default Multi<Entity> stream(String query, Sort sort, Object... params) {
    public default Multi<Entity> stream(String query, Map<String, Object> params) {
    public default Multi<Entity> stream(String query, Sort sort, Map<String, Object> params) {
    public default Multi<Entity> stream(String query, Parameters params) {
    public default Multi<Entity> stream(String query, Sort sort, Parameters params) {
    public default Multi<Entity> streamAll(Sort sort) {
    public default Multi<Entity> streamAll() {
    public default Uni<Long> count() {
    public default Uni<Long> count(String query, Object... params) {
    public default Uni<Long> count(String query, Map<String, Object> params) {
    public default Uni<Long> count(String query, Parameters params) {
    public default Uni<Long> deleteAll() {
    public default Uni<Boolean> deleteById(Id id) {
    public default Uni<Long> delete(String query, Object... params) {
    public default Uni<Long> delete(String query, Map<String, Object> params) {
    public default Uni<Long> delete(String query, Parameters params) {
    public default Uni<Void> persist(Iterable<Entity> entities) {
    public default Uni<Void> persist(Stream<Entity> entities) {
    public default Uni<Void> persist(Entity firstEntity, @SuppressWarnings("unchecked") Entity... entities) {
    public default Uni<Integer> update(String query, Object... params) {
    public default Uni<Integer> update(String query, Map<String, Object> params) {
    public default Uni<Integer> update(String query, Parameters params) {

ところで、今回作成したメソッドもそうなのですが、ドキュメントを見ているとJPQLを簡略化して書けるような雰囲気があります。

// finding all living persons
Uni<List<Person>> livingPersons = personRepository.list("status", Status.Alive);

// counting all living persons
Uni<Long> countAlive = personRepository.count("status", Status.Alive);

// delete all living persons
Uni<Long> deleteLivingOperation = personRepository.delete("status", Status.Alive);

// set the name of all living persons to 'Mortal'
Uni<Integer> updateOperation = personRepository.update("name = 'Mortal' where status = ?1", Status.Alive);

これは、クエリーとして渡される文字列の先頭を見て、以下のクラスで補完しているようです。

https://github.com/quarkusio/quarkus/blob/2.6.3.Final/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java

次は、設定を行います。最低限の設定は、Reactive SQL Clientを使える状態にすればOKです。

src/main/resources/application.properties

# Reactive SQL Client
quarkus.datasource.db-kind=mysql
quarkus.datasource.username=kazuhira
quarkus.datasource.password=password

# Reactive SQL Client
quarkus.datasource.reactive.url=mysql://172.17.0.2:3306/practice?characterEncoding=utf8mb4&charset=utf8mb4&collation=utf8mb4_bin

テストコードで、使い方を確認。

src/test/java/org/littlewings/quarkus/hibernate/reactive/BookRepositoryTest.java

package org.littlewings.quarkus.hibernate.reactive;

import java.util.List;
import javax.inject.Inject;

import io.quarkus.test.junit.QuarkusTest;
import io.smallrye.mutiny.helpers.test.UniAssertSubscriber;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;

@QuarkusTest
public class BookRepositoryTest {
    @Inject
    BookRepository bookRepository;

    @Test
    public void gettingStarted() {
        Book book1 = Book.create("978-1492091721", "Reactive Systems in Java: Resilient, Event-Driven Architecture With Quarkus", 7069);
        Book book2 = Book.create("978-1492062653", "Quarkus Cookbook: Kubernetes-optimized Java Solutions", 6379);
        Book book3 = Book.create("978-1484260319", "Beginning Quarkus Framework: Build Cloud-Native Enterprise Java Applications and Microservices", 5039);

        // insert
        UniAssertSubscriber<Book> persistAssertSubscriber =
                bookRepository
                        .persist(book1)
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        // count
        UniAssertSubscriber<Long> countAssertSubscriber =
                bookRepository
                        .count()
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        countAssertSubscriber
                .awaitItem()
                .assertItem(1L);

        // insert iterable
        UniAssertSubscriber<Void> assertMultiInsertSubscriber =
                bookRepository
                        .persist(List.of(book2, book3))
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());
        assertMultiInsertSubscriber.awaitItem().assertCompleted();

        // count
        countAssertSubscriber =
                bookRepository
                        .count()
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        countAssertSubscriber
                .awaitItem()
                .assertItem(3L);

        // find
        UniAssertSubscriber<Integer> findAssertSubscriber =
                bookRepository
                        .findByIsbn("978-1492091721")
                        .onItem()
                        .transform(book -> book.getPrice())
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        findAssertSubscriber
                .awaitItem()
                .assertCompleted()
                .assertItem(7069);

        // find query
        UniAssertSubscriber<List<Book>> findQueryAssertSubscriber =
                bookRepository
                        .findByPrice(6000)
                        .subscribe()
                        .withSerializedSubscriber(UniAssertSubscriber.create());

        List<Book> books = findQueryAssertSubscriber.awaitItem().getItem();
        assertThat(books, hasSize(2));
        assertThat(books.get(0).getTitle(), is("Reactive Systems in Java: Resilient, Event-Driven Architecture With Quarkus"));
        assertThat(books.get(0).getPrice(), is(7069));
        assertThat(books.get(1).getTitle(), is("Quarkus Cookbook: Kubernetes-optimized Java Solutions"));
        assertThat(books.get(1).getPrice(), is(6379));

        // delete
        UniAssertSubscriber<Boolean> deleteAssertSubscriber =
                bookRepository
                        .deleteById("978-1492091721")
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        deleteAssertSubscriber
                .awaitItem()
                .assertCompleted()
                .assertItem(true);

        // count
        countAssertSubscriber =
                bookRepository
                        .count()
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        countAssertSubscriber
                .awaitItem()
                .assertItem(2L);

        // delete all
        UniAssertSubscriber<Long> deleteAllAssertSubscriber =
                bookRepository
                        .deleteAll()
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        deleteAllAssertSubscriber.awaitItem();

        // count(empty)
        countAssertSubscriber =
                bookRepository
                        .count()
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        countAssertSubscriber
                .awaitItem()
                .assertItem(0L);
    }
}

ざっとこんな感じです。まあ、こっちは良かったのですが。

トランザクションを使ってみる

続いては、トランザクションを使ってみます。結果だけ言うと、ここでのロールバックの使い方がよくわかりませんでした。

Simplified Hibernate Reactive with Panache / Transactions

とりあえず、進めてみます。トランザクション境界は@ReactiveTransactionalアノテーションを付与するということなので、これを付与した
メソッドを持つサービスクラスを作成。

src/main/java/org/littlewings/quarkus/hibernate/reactive/BookService.java

package org.littlewings.quarkus.hibernate.reactive;

import java.util.function.Function;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional;
import io.smallrye.common.annotation.Blocking;
import io.smallrye.mutiny.Uni;

@ApplicationScoped
public class BookService {
    @Inject
    BookRepository bookRepository;

    @ReactiveTransactional
    public <T> Uni<T> withTransaction(Function<BookRepository, Uni<T>> function) {
        return function.apply(bookRepository);
    }

    public <T> Uni<T> nonTransaction(Function<BookRepository, Uni<T>> function) {
        return function.apply(bookRepository);
    }
}

メソッドの中身が面倒になったので、今回はRepositoryをFunctionで操作させることにしました。

比較のために、@ReactiveTransactionalアノテーションを付与していないメソッドも作成。

テストコードはこちら。

src/test/java/org/littlewings/quarkus/hibernate/reactive/TransactionalTest.java

package org.littlewings.quarkus.hibernate.reactive;

import java.time.Duration;
import java.util.Arrays;
import javax.inject.Inject;

import io.quarkus.test.junit.QuarkusTest;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.helpers.test.UniAssertSubscriber;
import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.fail;

@QuarkusTest
public class TransactionalTest {
    @Inject
    BookService bookService;

    @Test
    public void commit() {
        Book book1 = Book.create("978-1492091721", "Reactive Systems in Java: Resilient, Event-Driven Architecture With Quarkus", 7069);
        Book book2 = Book.create("978-1492062653", "Quarkus Cookbook: Kubernetes-optimized Java Solutions", 6379);
        Book book3 = Book.create("978-1484260319", "Beginning Quarkus Framework: Build Cloud-Native Enterprise Java Applications and Microservices", 5039);

        Uni<Book> commit = bookService.withTransaction(bookRepository ->
                bookRepository
                        .persist(book1)
                        .onItem()
                        .transformToUni(b -> bookRepository.persist(book2))
                        .onItem()
                        .transformToUni(b -> bookRepository.persist(book3))
        );

        commit.await().indefinitely();

        UniAssertSubscriber<Long> countSubscriber =
                bookService
                        .withTransaction(bookRepository -> bookRepository.count())
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        countSubscriber
                .awaitItem()
                .assertItem(3L);

        bookService.withTransaction(bookRepository -> bookRepository.deleteAll()).await().indefinitely();
    }

    @Test
    public void rollback() throws InterruptedException {
        Book book1 = Book.create("978-1492091721", "Reactive Systems in Java: Resilient, Event-Driven Architecture With Quarkus", 7069);
        Book book2 = Book.create("978-1492062653", "Quarkus Cookbook: Kubernetes-optimized Java Solutions", 6379);

        try {
            Uni<Book> rollback = bookService.withTransaction(bookRepository ->
                    bookRepository
                            .persist(book1)
                            .onItem()
                            .transformToUni(b -> bookRepository.persist(book2))
                            .onItem()
                            .delayIt()
                            .by(Duration.ofSeconds(3L))
                            .onItem()
                            .transform(b -> {
                                throw new RuntimeException("Oops!!");
                            })
            );

            rollback.await().indefinitely();

            fail("fail");
        } catch (Exception e) {
            assertThat(e.getCause(), instanceOf(RuntimeException.class));
            assertThat(e.getCause().getMessage(), is("Oops!!"));
            assertThat(Arrays.asList(e.getSuppressed()), hasSize(1));
            assertThat(e.getSuppressed()[0], instanceOf(IllegalStateException.class));
            assertThat(e.getSuppressed()[0].getMessage(), startsWith("HR000068: This method should exclusively be invoked from a Vert.x EventLoop thread; currently running on thread 'executor-thread"));
        }

        UniAssertSubscriber<Long> countSubscriber =
                bookService
                        .withTransaction(bookRepository -> bookRepository.count())
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        countSubscriber
                .awaitFailure()
                .assertFailedWith(IllegalStateException.class);
    }

    @Test
    public void nonTransaction() {
        Book book1 = Book.create("978-1492091721", "Reactive Systems in Java: Resilient, Event-Driven Architecture With Quarkus", 7069);
        Book book2 = Book.create("978-1492062653", "Quarkus Cookbook: Kubernetes-optimized Java Solutions", 6379);

        try {
            Uni<Book> nonTransaction = bookService.nonTransaction(bookRepository ->
                    bookRepository
                            .persist(book1)
                            .onItem()
                            .transformToUni(b -> bookRepository.persist(book2))
                            .onItem()
                            .delayIt()
                            .by(Duration.ofSeconds(3L))
                            .onItem()
                            .transform(b -> {
                                throw new RuntimeException("Oops!!");
                            })
            );

            nonTransaction.await().indefinitely();  // throw RuntimeException

            fail("fail");
        } catch (Exception e) {
            assertThat(e.getMessage(), is("Oops!!"));
        }

        UniAssertSubscriber<Long> countSubscriber =
                bookService
                        .withTransaction(bookRepository -> bookRepository.count())
                        .subscribe()
                        .withSubscriber(UniAssertSubscriber.create());

        countSubscriber
                .awaitItem()
                .assertItem(2L);

        bookService.nonTransaction(bookRepository -> bookRepository.deleteAll()).await().indefinitely();
    }
}

コミットするパターンと、そもそもトランザクションを使わないパターンはまだいいのですが。

ロールバックする方が同じようなコードで結果をawaitするとブロックするなと怒られるし、その後でトランザクションを使おうとすると
失敗するしで、よくわからない感じになりました。

あと、これ以上追わなくてもいいかなという気分になったので、ここで終わりにします。

まとめ

QuarkusのHibernate Reactive with Panache Extensionを試してみましたが、テストコードだとトランザクションまわりで苦労することに
なって今回はほどほどでやめることにしました。

REST APIとして作って確認していたら、こうはならなかったかもしれませんが。Hibernate ReactiveもPanacheもどういうものかは
ある程度わかった気がするので、扱うならまたそのうちにしましょう。