これは、なにをしたくて書いたもの?
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のリアクティブデータベースクライアントを使用します。
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も存在はするようですが。
よく見ると、両方ともPreviewですね…code.quarkus.io
を見て気づきました…。
テストでのトランザクションに関しては、JTAであれば@TestTransaction
というアノテーションがあるようですが、
Hibernate Reactive with Panacheには対応するものがドキュメントにはなさそうです。
このあたりがそれっぽいかな、と思いますが、ドキュメントには書かれていないのでいったんメモのみにします。
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が進化したら、また見てみようかなと思います。
環境
今回の環境は、こちら。
$ 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
インターフェースのソースコードを見ればわかるのですが
デフォルトメソッドを書き出すと、こんな感じですね。
$ 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);
これは、クエリーとして渡される文字列の先頭を見て、以下のクラスで補完しているようです。
次は、設定を行います。最低限の設定は、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もどういうものかは
ある程度わかった気がするので、扱うならまたそのうちにしましょう。