The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

PanacheでシンプルになったHibernate Reactive

Hibernate Reactiveは 、唯一のリアクティブ Jakarta Persistence(旧JPA)実装で、リアクティブドライバでデータベースにアクセスできるObject Relational Mapperの全機能を提供します。複雑なマッピングを可能にしますが、単純で一般的なマッピングを容易なものにするわけではありません。Hibernate Reactive with Panacheは、Quarkusでエンティティを簡単に、楽しく書けるようにすることに重点を置いています。

Hibernate Reactive is not a replacement for Hibernate ORM or the future of Hibernate ORM. It is a different stack tailored for reactive use cases where you need high-concurrency.

Furthermore, using Quarkus REST (formerly RESTEasy Reactive), our default REST layer, does not require the use of Hibernate Reactive. It is perfectly valid to use Quarkus REST with Hibernate ORM, and if you do not need high-concurrency, or are not accustomed to the reactive paradigm, it is recommended to use Hibernate ORM.

最初に:例

Panacheでは、HibernateのReactiveエンティティをこのように書けるようにしています:

import io.quarkus.hibernate.reactive.panache.PanacheEntity;

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Uni<Person> findByName(String name){
        return find("name", name).firstResult();
    }

    public static Uni<List<Person>> findAlive(){
        return list("status", Status.Alive);
    }

    public static Uni<Long> deleteStefs(){
        return delete("name", "Stef");
    }
}

コードがどれだけコンパクトで読みやすくなっているかお気づきですか?面白いと思いませんか?読んでみてください。

list() メソッドには、最初は驚くかもしれません。これはHQL(JP-QL)クエリのフラグメントを取り、残りをコンテキスト化するものです。そのため、非常に簡潔で、しかも読みやすいコードになります。
上記で説明したのは、基本的に アクティブレコードパターン であり、単にエンティティパターンと呼ばれることもあります。Hibernate with Panacheでは、 PanacheRepository を介して、より古典的な リポジトリパターン を使用することも可能です。

ソリューション

次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

Gitレポジトリをクローンするか git clone https://github.com/quarkusio/quarkus-quickstarts.gitアーカイブ をダウンロードします。

ソリューションは hibernate-reactive-panache-quickstart ディレクトリ にあります。

PanacheによるHibernate Reactiveのセットアップと設定

始めるには:

  • application.properties で設定を追加します

  • エンティティに @Entity アノテーションを付けます

  • エンティティが PanacheEntity を拡張するようにする(リポジトリパターンを使用している場合はオプションです)

すべての設定は、 Hibernateセットアップガイドを確認してください。

ビルドファイルに、以下の依存関係を追加します:

  • Hibernate Reactive with Panache エクステンション

  • お使いのリアクティブドライバのエクステンション ( quarkus-reactive-pg-client , quarkus-reactive-mysql-client , quarkus-reactive-db2-client , …​ )

例えば:

pom.xml
<!-- Hibernate Reactive dependency -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-reactive-panache</artifactId>
</dependency>

<!-- Reactive SQL client for PostgreSQL -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
build.gradle
// Hibernate Reactive dependency
implementation("io.quarkus:quarkus-hibernate-reactive-panache")

Reactive SQL client for PostgreSQL
implementation("io.quarkus:quarkus-reactive-pg-client")

次に、 application.properties で関連する設定プロパティを追加します。

# configure your datasource
quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = sarah
quarkus.datasource.password = connor
quarkus.datasource.reactive.url = vertx-reactive:postgresql://localhost:5432/mydatabase

# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation = drop-and-create

解決策1:アクティブレコードパターンを使用する

エンティティの定義

Panache エンティティーを定義するには、 PanacheEntity を拡張して @Entity とアノテーションを付け、列をパブリック フィールドとして追加します。

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;
}

publicフィールドには、すべてのJakarta Persistenceのカラムアノテーションを付けることができます。永続化しないフィールドが必要な場合は、 @Transient アノテーションをそのフィールドに使用します。アクセサーを書く必要がある場合は、次のようにできます:

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    // return name as uppercase in the model
    public String getName(){
        return name.toUpperCase();
    }

    // store all names in lowercase in the DB
    public void setName(String name){
        this.name = name.toLowerCase();
    }
}

また、当社のフィールドアクセスリライトのおかげで、ユーザーが person.name を読むときには、実際に getName() アクセサが呼び出されます。これはフィールドの書き込みやセッターについても同様です。これにより、すべてのフィールドの呼び出しが、対応するゲッター/セッターの呼び出しに置き換えられるため、実行時に適切なカプセル化が可能になります。

最も使うことの多い操作

エンティティーを記述したら、このように最も一般的な操作が実行できるようになります:

// creating a person
Person person = new Person();
person.name = "Stef";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;

// persist it
Uni<Void> persistOperation = person.persist();

// note that once persisted, you don't need to explicitly save your entity: all
// modifications are automatically persisted on transaction commit.

// check if it is persistent
if(person.isPersistent()){
    // delete it
    Uni<Void> deleteOperation = person.delete();
}

// getting a list of all Person entities
Uni<List<Person>> allPersons = Person.listAll();

// finding a specific person by ID
Uni<Person> personById = Person.findById(23L);

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

// counting all persons
Uni<Long> countAll = Person.count();

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

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

// delete all persons
Uni<Long> deleteAllOperation = Person.deleteAll();

// delete by id
Uni<Boolean> deleteByIdOperation = Person.deleteById(23L);

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

エンティティメソッドの追加

エンティティに対するカスタムクエリを、エンティティ自体の中に追加できます。そうすることで、自分や同僚が簡単に見つけることができ、クエリは操作するオブジェクトと一緒に配置されます。エンティティクラスにスタティックメソッドとして追加するのがPanache Active Recordのやり方です。

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Uni<Person> findByName(String name){
        return find("name", name).firstResult();
    }

    public static Uni<List<Person>> findAlive(){
        return list("status", Status.Alive);
    }

    public static Uni<Long> deleteStefs(){
        return delete("name", "Stef");
    }
}

解決策2:リポジトリパターンを使用する

エンティティの定義

リポジトリパターンを使用する場合、エンティティを通常のJakarta Persistenceエンティティとして定義することができます。

@Entity
public class Person {
    @Id @GeneratedValue private Long id;
    private String name;
    private LocalDate birth;
    private Status status;

    public Long getId(){
        return id;
    }
    public void setId(Long id){
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public LocalDate getBirth() {
        return birth;
    }
    public void setBirth(LocalDate birth) {
        this.birth = birth;
    }
    public Status getStatus() {
        return status;
    }
    public void setStatus(Status status) {
        this.status = status;
    }
}
エンティティにゲッター/セッターを定義するのが面倒な場合は、 PanacheEntityBase を拡張するようにすればQuarkusが生成してくれます。また、 PanacheEntity を拡張して、デフォルトのIDを利用することもできます。

リポジトリの定義

リポジトリを使用する場合、PanacheRepository を実装することでアクティブレコードパターンとまったく同じ便利なメソッドをリポジトリにインジェクションできます:

@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {

   // put your custom logic here as instance methods

   public Uni<Person> findByName(String name){
       return find("name", name).firstResult();
   }

   public Uni<List<Person>> findAlive(){
       return list("status", Status.Alive);
   }

   public Uni<Long> deleteStefs(){
       return delete("name", "Stef");
  }
}

PanacheEntityBase で定義されている操作はすべてリポジトリ上で利用可能なので、これを使用することはアクティブレコードパターンを使用するのと全く同じですが、それを注入する必要があります。

@Inject
PersonRepository personRepository;

@GET
public Uni<Long> count(){
    return personRepository.count();
}

最も使うことの多い操作

リポジトリを書くことで実行可能な最も一般的な操作は以下の通りです:

// creating a person
Person person = new Person();
person.setName("Stef");
person.setBirth(LocalDate.of(1910, Month.FEBRUARY, 1));
person.setStatus(Status.Alive);

// persist it
Uni<Void> persistOperation = personRepository.persist(person);

// note that once persisted, you don't need to explicitly save your entity: all
// modifications are automatically persisted on transaction commit.

// check if it is persistent
if(personRepository.isPersistent(person)){
    // delete it
    Uni<Void> deleteOperation = personRepository.delete(person);
}

// getting a list of all Person entities
Uni<List<Person>> allPersons = personRepository.listAll();

// finding a specific person by ID
Uni<Person> personById = personRepository.findById(23L);

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

// counting all persons
Uni<Long> countAll = personRepository.count();

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

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

// delete all persons
Uni<Long> deleteAllOperation = personRepository.deleteAll();

// delete by id
Uni<Boolean> deleteByIdOperation = personRepository.deleteById(23L);

// set the name of all living persons to 'Mortal'
Uni<Integer> updateOperation = personRepository.update("name = 'Mortal' where status = ?1", Status.Alive);
残りのドキュメントでは、アクティブレコードパターンに基づく使用法のみを示していますが、リポジトリパターンでも実行できることを覚えておいてください。リポジトリパターンの例は簡潔にするために省略しています。

高度なクエリー

ページング

list メソッドは、テーブルに含まれるデータセットが十分に小さい場合にのみ使用してください。より大きなデータセットの場合は、同等の find メソッドを使用して、ページングが可能な PanacheQuery を返すことができます:

// create a query for all living persons
PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);

// make it use pages of 25 entries at a time
livingPersons.page(Page.ofSize(25));

// get the first page
Uni<List<Person>> firstPage = livingPersons.list();

// get the second page
Uni<List<Person>> secondPage = livingPersons.nextPage().list();

// get page 7
Uni<List<Person>> page7 = livingPersons.page(Page.of(7, 25)).list();

// get the number of pages
Uni<Integer> numberOfPages = livingPersons.pageCount();

// get the total number of entities returned by this query without paging
Uni<Long> count = livingPersons.count();

// and you can chain methods of course
Uni<List<Person>> persons = Person.find("status", Status.Alive)
        .page(Page.ofSize(25))
        .nextPage()
        .list();

PanacheQuery 型には、ページングや返されたストリームを処理するための他の多くのメソッドがあります。

ページの代わりにレンジを使用

PanacheQuery では、レンジベースのクエリーも使用できます。

// create a query for all living persons
PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);

// make it use a range: start at index 0 until index 24 (inclusive).
livingPersons.range(0, 24);

// get the range
Uni<List<Person>> firstRange = livingPersons.list();

// to get the next range, you need to call range again
Uni<List<Person>> secondRange = livingPersons.range(25, 49).list();

範囲とページを混在させることはできません。範囲を使用した場合、現在のページを持っていることに依存するすべてのメソッドは UnsupportedOperationException をスローします。page(Page) もしくは page(int, int) を使用してページングに切り換えられます。

ソート

クエリー文字列を受け付けるすべてのメソッドは、以下の簡略化されたクエリー形式も受け付けます:

Uni<List<Person>> persons = Person.list("order by name,birth");

しかし、これらのメソッドには、オプションで Sort というパラメータが用意されており、これによってソートの抽象化が可能になります:

Uni<List<Person>> persons = Person.list(Sort.by("name").and("birth"));

// and with more restrictions
Uni<List<Person>> persons = Person.list("status", Sort.by("name").and("birth"), Status.Alive);

// and list first the entries with null values in the field "birth"
Uni<List<Person>> persons = Person.list(Sort.by("birth", Sort.NullPrecedence.NULLS_FIRST));

Sort クラスには、列を追加したり、ソート方向を指定したり、nullの優先順位を指定したりするメソッドが豊富に用意されています。

シンプルなクエリー

通常、HQLのクエリは from EntityName [where …​] [order by …​] というように最後にオプションの要素を持つという形式になっています。

If your select query does not start with from, select or with, we support the following additional forms:

  • order by …​from EntityName order by …​ に展開されます

  • <singleAttribute> (and single parameter) which will expand to from EntityName where <singleAttribute> = ?

  • where <query> will expand to from EntityName where <query>

  • <query>from EntityName where <query> に展開されます

更新クエリーが update で始まらない場合は、以下の追加の形式をサポートしています:

  • from EntityName …​` は update EntityName …​ に展開されます

  • set? <singleAttribute> (and single parameter) which will expand to update EntityName set <singleAttribute> = ?

  • set? <update-query>update EntityName set <update-query> に展開されます

削除クエリーが delete で始まらない場合は、以下の追加の形式をサポートしています:

  • from EntityName …​delete from EntityName …​ に展開されます

  • <singleAttribute> (and single parameter) which will expand to delete from EntityName where <singleAttribute> = ?

  • <query>delete from EntityName where <query> に展開されます

また、プレーンな HQL でクエリを記述することもできます:
Order.find("select distinct o from Order o left join fetch o.lineItems");
Order.update("update from Person set name = 'Mortal' where status = ?", Status.Alive);

名前付きクエリー

名前付きのクエリーは、その名前の前に「#」文字を付けることで、(簡易)HQLクエリーの代わりに参照することができます。また、名前付きのクエリーは、カウント、更新、削除のクエリーにも使用できます。

@Entity
@NamedQueries({
    @NamedQuery(name = "Person.getByName", query = "from Person where name = ?1"),
    @NamedQuery(name = "Person.countByStatus", query = "select count(*) from Person p where p.status = :status"),
    @NamedQuery(name = "Person.updateStatusById", query = "update Person p set p.status = :status where p.id = :id"),
    @NamedQuery(name = "Person.deleteById", query = "delete from Person p where p.id = ?1")
})
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Uni<Person> findByName(String name){
        return find("#Person.getByName", name).firstResult();
    }

    public static Uni<Long> countByStatus(Status status) {
        return count("#Person.countByStatus", Parameters.with("status", status).map());
    }

    public static Uni<Long> updateStatusById(Status status, Long id) {
        return update("#Person.updateStatusById", Parameters.with("status", status).and("id", id));
    }

    public static Uni<Long> deleteById(Long id) {
        return delete("#Person.deleteById", id);
    }
}

名前付きクエリは、Jakarta Persistenceエンティティ・クラス(Panacheエンティティ・クラス、またはリポジトリのパラメータ化型)内部、またはそのスーパー・クラスの1つでしか定義できません。

クエリーパラメーター

以下のように、インデックス(1ベース)でクエリーパラメーターを渡すことができます:

Person.find("name = ?1 and status = ?2", "stef", Status.Alive);

または、 Map を使った名前で:

Map<String, Object> params = new HashMap<>();
params.put("name", "stef");
params.put("status", Status.Alive);
Person.find("name = :name and status = :status", params);

または、便利なクラス Parameters をそのまま使用するか、 Map を構築する:

// generate a Map
Person.find("name = :name and status = :status",
         Parameters.with("name", "stef").and("status", Status.Alive).map());

// use it as-is
Person.find("name = :name and status = :status",
         Parameters.with("name", "stef").and("status", Status.Alive));

すべてのクエリー操作は、インデックス( Object…​)または名前付きパラメーター( Map<String,Object> または Parameters)でパラメータを渡すことができます。

クエリーの射影

クエリーの射影は、 find() のメソッドが返す PanacheQuery オブジェクトに対して project(Class) のメソッドで行うことができます。

これを使って、データベースから返されるフィールドを制限することができます。

Hibernateは DTOプロジェクション を使用し、プロジェクションクラスからの属性を持つSELECT句を生成します。これは 動的インスタンス化 または コンストラクタ式 とも呼ばれ、詳細は Hibernate ガイドの hql select 節 を参照してください。

射影クラスは、有効な Java Bean であり、すべての属性を含むコンストラクタを持つ必要があります。このコンストラクタは、エンティティクラスを使用する代わりに、射影のDTOをインスタンス化するために使用されます。このクラスは、すべてのクラス属性をパラメータとして持つ一致するコンストラクタを持つ必要があります。

import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection (1)
public class PersonName {
    public final String name; (2)

    public PersonName(String name){ (3)
        this.name = name;
    }
}

// only 'name' will be loaded from the database
PanacheQuery<PersonName> query = Person.find("status", Status.Alive).project(PersonName.class);
1 @RegisterForReflection アノテーションは、ネイティブコンパイル時にクラスとそのメンバーを保持するようQuarkusに指示します。 @RegisterForReflection アノテーションの詳細については、 ネイティブアプリケーションのヒントのページを参照してください。
2 ここではパブリックフィールドを使用していますが、必要に応じてプライベートフィールドやゲッター/セッターを使用することもできます。
3 このコンストラクタはHibernate によって使用されます。このコンストラクタはクラス内の唯一のコンストラクタであり、パラメータとしてクラスのすべての属性を持つ必要があります。

project(Class) メソッドの実装では、コンストラクタのパラメータ名を使用してクエリーの select 節を構築するため、コンパイルされたクラスの中にパラメータ名を格納するようにコンパイラを設定する必要があります。Quarkus Mavenアーキタイプを使用している場合はデフォルトで有効になっています。使用していない場合はプロパティ <maven.compiler.parameters>true</maven.compiler.parameters>pom.xml に追加してください。

DTO射影のオブジェクトから参照されるエンティティのフィールドがある場合、 @ProjectedFieldName アノテーションを使用してSELECT文のパスを提供することができます。

@Entity
public class Dog extends PanacheEntity {
    public String name;
    public String race;
    public Double weight;
    @ManyToOne
    public Person owner;
}

@RegisterForReflection
public class DogDto {
    public String name;
    public String ownerName;

    public DogDto(String name, @ProjectedFieldName("owner.name") String ownerName) {  (1)
        this.name = name;
        this.ownerName = ownerName;
    }
}

PanacheQuery<DogDto> query = Dog.findAll().project(DogDto.class);
1 ownerName DTOコンストラクタのパラメータは owner.name HQLプロパティから読み込まれます。

In case you want to project an entity in a class with nested classes, you can use the @NestedProjectedClass annotation on those nested classes.

@RegisterForReflection
public class DogDto {
    public String name;
    public PersonDto owner;

    public DogDto(String name, PersonDto owner) {
        this.name = name;
        this.owner = owner;
    }

    @NestedProjectedClass (1)
    public static class PersonDto {
        public String name;

        public PersonDto(String name) {
            this.name = name;
        }
    }
}

PanacheQuery<DogDto> query = Dog.findAll().project(DogDto.class);
1 This annotation can be used when you want to project @Embedded entity or @ManyToOne, @OneToOne relation. It does not support @OneToMany or @ManyToMany relation.

また、select句でHQLクエリを指定できます。この場合、射影クラスは、select句が返す値に一致するコンストラクタを持つ必要があります。

import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection
public class RaceWeight {
    public final String race;
    public final Double weight

    public RaceWeight(String race) {
        this(race, null);
    }

    public RaceWeight(String race, Double weight) { (1)
        this.race = race;
        this.weight = weight;
    }
}

// Only the race and the average weight will be loaded
PanacheQuery<RaceWeight> query = Person.find("select d.race, AVG(d.weight) from Dog d group by d.race").project(RaceWeight.class);
1 Hibernate Reactive は、このコンストラクタを使用します。クエリが select 節を持つ場合、複数のコンストラクタを持つことが可能です。

HQL select new クエリと .project(Class) を同時に行うことはできません。どちらかの方法を選択する必要があります。

例えば、このような場合、失敗します。

PanacheQuery<RaceWeight> query = Person.find("select new MyView(d.race, AVG(d.weight)) from Dog d group by d.race").project(AnotherView.class);

複数の永続性ユニット

QuarkusのHibernate Reactiveは現時点では複数の永続化ユニットをサポートしていません。

セッションとトランザクション

First of all, most of the methods of a Panache entity must be invoked within the scope of a reactive Mutiny.Session. In some cases, the session is opened automatically on demand. For example, if a Panache entity method is invoked in a Jakarta REST resource method in an application that includes the quarkus-rest extension. For other cases, there are both a declarative and a programmatic way to ensure the session is opened. You can annotate a CDI business method that returns Uni with the @WithSession annotation. The method will be intercepted and the returned Uni will be triggered within a scope of a reactive session. Alternatively, you can use the Panache.withSession() method to achieve the same effect.

Note that a Panache entity may not be used from a blocking thread. See also Getting Started With Reactive guide that explains the basics of reactive principles in Quarkus.

Also make sure to wrap methods that modify the database or involve multiple queries (e.g. entity.persist()) within a transaction. You can annotate a CDI business method that returns Uni with the @WithTransaction annotation. The method will be intercepted and the returned Uni is triggered within a transaction boundary. Alternatively, you can use the Panache.withTransaction() method for the same effect.

You cannot use the @Transactional annotation with Hibernate Reactive for your transactions: you must use @WithTransaction, and your annotated method must return a Uni to be non-blocking.

Hibernate Reactive batches changes you make to your entities and sends changes (it is called flush) at the end of the transaction or before a query. This is usually a good thing as it is more efficient. But if you want to check optimistic locking failures, do object validation right away or generally want to get immediate feedback, you can force the flush operation by calling entity.flush() or even use entity.persistAndFlush() to make it a single method call. This will allow you to catch any PersistenceException that could occur when Hibernate Reactive send those changes to the database. Remember, this is less efficient so don’t abuse it. And your transaction still has to be committed.

ここでは PersistenceException が発生した場合に特定の動作を行えるようにするための flush メソッドの使用例を示します:

@WithTransaction
public Uni<Void> create(Person person){
    // Here we use the persistAndFlush() shorthand method on a Panache repository to persist to database then flush the changes.
    return person.persistAndFlush()
            .onFailure(PersistenceException.class)
            .recoverWithItem(() -> {
                LOG.error("Unable to create the parameter", pe);
                //in case of error, I save it to disk
                diskPersister.save(person);
                return null;
            });
}

The @WithTransaction annotation will also work for testing. This means that changes done during the test will be propagated to the database. If you want any changes made to be rolled back at the end of the test you can use the io.quarkus.test.TestReactiveTransaction annotation. This will run the test method in a transaction, but roll it back once the test method is complete to revert any database changes.

ロック管理

Panacheは findById(Object, LockModeType)find().withLock(LockModeType) を使用してエンティティ/リポジトリでデータベースロックを直接サポートします。

以下の例はアクティブレコードパターンの場合ですが、リポジトリでも同じように使用できます。

1つ目: findById()を使ってロックする。

public class PersonEndpoint {

    @GET
    public Uni<Person> findByIdForUpdate(Long id){
        return Panache.withTransaction(() -> {
            return Person.<Person>findById(id, LockModeType.PESSIMISTIC_WRITE)
                    .invoke(person -> {
                        //do something useful, the lock will be released when the transaction ends.
                    });
        });
    }
}

2つ目:find()でロックする。

public class PersonEndpoint {

    @GET
    public Uni<Person> findByNameForUpdate(String name){
        return Panache.withTransaction(() -> {
            return Person.<Person>find("name", name).withLock(LockModeType.PESSIMISTIC_WRITE).firstResult()
                    .invoke(person -> {
                        //do something useful, the lock will be released when the transaction ends.
                    });
        });
    }

}

トランザクションが終了するとロックが解放されるため、ロッククエリーを呼び出すメソッドはトランザクション内で呼び出す必要があることに注意してください。

カスタムID

IDは微妙な問題で、誰もがフレームワークに任せることができるわけではありませんが、今回も私たちはカバーします。

PanacheEntity の代わりに PanacheEntityBase を拡張することで独自のID戦略を指定することができます。そのあとに好きなIDをパブリック・フィールドとして宣言するだけです:

@Entity
public class Person extends PanacheEntityBase {

    @Id
    @SequenceGenerator(
            name = "personSequence",
            sequenceName = "person_id_seq",
            allocationSize = 1,
            initialValue = 4)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "personSequence")
    public Integer id;

    //...
}

リポジトリを使用している場合は PanacheRepository の代わりに PanacheRepositoryBase を拡張し、IDの型を追加の型パラメーターとして指定することになります:

@ApplicationScoped
public class PersonRepository implements PanacheRepositoryBase<Person,Integer> {
    //...
}

テスト

@QuarkusTest におけるリアクティブPanacheエンティティのテストは、APIの非同期性と、すべての操作がVert.xイベントループ上で実行される必要があるという事実のために、通常のPanacheエンティティのテストよりも若干複雑です。

quarkus-test-vertx 依存関係は、まさにこの目的のために @io.quarkus.test.vertx.RunOnVertxContext アノテーションと io.quarkus.test.vertx.UniAsserter クラスを提供します。使用方法は、 Hibernate Reactive ガイドに記載されています。

Moreover, the quarkus-test-hibernate-reactive-panache dependency provides the io.quarkus.test.hibernate.reactive.panache.TransactionalUniAsserter that can be injected as a method parameter of a test method annotated with @RunOnVertxContext. The TransactionalUniAsserter is a io.quarkus.test.vertx.UniAsserterInterceptor that wraps each assert method within a separate reactive transaction.

TransactionalUniAsserter
import io.quarkus.test.hibernate.reactive.panache.TransactionalUniAsserter;

@QuarkusTest
public class SomeTest {

    @Test
    @RunOnVertxContext
    public void testEntity(TransactionalUniAsserter asserter) {
        asserter.execute(() -> new MyEntity().persist()); (1)
        asserter.assertEquals(() -> MyEntity.count(), 1l); (2)
        asserter.execute(() -> MyEntity.deleteAll()); (3)
    }
}
1 The first reactive transaction is used to persist the entity.
2 The second reactive transaction is used to count the entities.
3 The third reactive transaction is used to delete all entities.

Of course, you can also define a custom UniAsserterInterceptor to wrap the injected UniAsserter and customize the behavior.

モック

アクティブレコードパターンの使用

アクティブレコードパターンを使用している場合、Mockitoは静的メソッドのモックをサポートしていないため、直接使用することはできませんが、 quarkus-panache-mock モジュールを使用することで、Mockitoを使用して、あなた自身のメソッドを含む、提供されたすべての静的メソッドをモックすることができます。

この依存関係をビルドファイルに追加してください:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-panache-mock</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-panache-mock")

このシンプルなエンティティ:

@Entity
public class Person extends PanacheEntity {

    public String name;

    public static Uni<List<Person>> findOrdered() {
        return find("ORDER BY name").list();
    }
}

モック化テストはこのように書くことができます:

import io.quarkus.test.vertx.UniAsserter;
import io.quarkus.test.vertx.RunOnVertxContext;

@QuarkusTest
public class PanacheFunctionalityTest {

    @RunOnVertxContext (1)
    @Test
    public void testPanacheMocking(UniAsserter asserter) { (2)
        asserter.execute(() -> PanacheMock.mock(Person.class));

        // Mocked classes always return a default value
        asserter.assertEquals(() -> Person.count(), 0l);

        // Now let's specify the return value
        asserter.execute(() -> Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(23l)));
        asserter.assertEquals(() -> Person.count(), 23l);

        // Now let's change the return value
        asserter.execute(() -> Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(42l)));
        asserter.assertEquals(() -> Person.count(), 42l);

        // Now let's call the original method
        asserter.execute(() -> Mockito.when(Person.count()).thenCallRealMethod());
        asserter.assertEquals(() -> Person.count(), 0l);

        // Check that we called it 4 times
        asserter.execute(() -> {
            PanacheMock.verify(Person.class, Mockito.times(4)).count(); (3)
        });

        // Mock only with specific parameters
        asserter.execute(() -> {
            Person p = new Person();
            Mockito.when(Person.findById(12l)).thenReturn(Uni.createFrom().item(p));
            asserter.putData(key, p);
        });
        asserter.assertThat(() -> Person.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key)));
        asserter.assertNull(() -> Person.findById(42l));

        // Mock throwing
        asserter.execute(() -> Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException()));
        asserter.assertFailedWith(() -> {
            try {
                return Person.findById(12l);
            } catch (Exception e) {
                return Uni.createFrom().failure(e);
            }
        }, t -> assertEquals(WebApplicationException.class, t.getClass()));

        // We can even mock your custom methods
        asserter.execute(() -> Mockito.when(Person.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList())));
        asserter.assertThat(() -> Person.findOrdered(), list -> list.isEmpty());

        asserter.execute(() -> {
            PanacheMock.verify(Person.class).findOrdered();
            PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any());
            PanacheMock.verifyNoMoreInteractions(Person.class);
        });

        // IMPORTANT: We need to execute the asserter within a reactive session
        asserter.surroundWith(u -> Panache.withSession(() -> u));
    }
}
1 テストメソッドがVert.xのイベントループで実行されるようにします。
2 The injected UniAsserter argument is used to make assertions.
3 verifydo* のメソッドは Mockito ではなく PanacheMock で呼び出すようにしてください。そうしないとどのモックオブジェクトを渡せばいいのかわからなくなってしまいます。

リポジトリパターンの使用

リポジトリパターンを使用している場合は、 quarkus-junit5-mockito モジュールを使用して、Mockito を直接使用することができます。これにより、ビーンのモッキングが非常に簡単になります。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-junit5-mockito")

このシンプルなエンティティ:

@Entity
public class Person {

    @Id
    @GeneratedValue
    public Long id;

    public String name;
}

そしてこのリポジトリ:

@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {
    public Uni<List<Person>> findOrdered() {
        return find("ORDER BY name").list();
    }
}

モック化テストはこのように書くことができます:

import io.quarkus.test.vertx.UniAsserter;
import io.quarkus.test.vertx.RunOnVertxContext;

@QuarkusTest
public class PanacheFunctionalityTest {
    @InjectMock
    PersonRepository personRepository;

    @RunOnVertxContext (1)
    @Test
    public void testPanacheRepositoryMocking(UniAsserter asserter) { (2)

        // Mocked classes always return a default value
        asserter.assertEquals(() -> mockablePersonRepository.count(), 0l);

        // Now let's specify the return value
        asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(23l)));
        asserter.assertEquals(() -> mockablePersonRepository.count(), 23l);

        // Now let's change the return value
        asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(42l)));
        asserter.assertEquals(() -> mockablePersonRepository.count(), 42l);

        // Now let's call the original method
        asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenCallRealMethod());
        asserter.assertEquals(() -> mockablePersonRepository.count(), 0l);

        // Check that we called it 4 times
        asserter.execute(() -> {
            Mockito.verify(mockablePersonRepository, Mockito.times(4)).count();
        });

        // Mock only with specific parameters
        asserter.execute(() -> {
            Person p = new Person();
            Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(Uni.createFrom().item(p));
            asserter.putData(key, p);
        });
        asserter.assertThat(() -> mockablePersonRepository.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key)));
        asserter.assertNull(() -> mockablePersonRepository.findById(42l));

        // Mock throwing
        asserter.execute(() -> Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException()));
        asserter.assertFailedWith(() -> {
            try {
                return mockablePersonRepository.findById(12l);
            } catch (Exception e) {
                return Uni.createFrom().failure(e);
            }
        }, t -> assertEquals(WebApplicationException.class, t.getClass()));

        // We can even mock your custom methods
        asserter.execute(() -> Mockito.when(mockablePersonRepository.findOrdered())
                .thenReturn(Uni.createFrom().item(Collections.emptyList())));
        asserter.assertThat(() -> mockablePersonRepository.findOrdered(), list -> list.isEmpty());

        asserter.execute(() -> {
            Mockito.verify(mockablePersonRepository).findOrdered();
            Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any());
            Mockito.verify(mockablePersonRepository).persist(Mockito.<Person> any());
            Mockito.verifyNoMoreInteractions(mockablePersonRepository);
        });

        // IMPORTANT: We need to execute the asserter within a reactive session
        asserter.surroundWith(u -> Panache.withSession(() -> u));
    }
}
1 テストメソッドがVert.xのイベントループで実行されるようにします。
2 The injected UniAsserter agrument is used to make assertions.

HibernateのReactiveマッピングを単純化する方法と理由

HibernateのReactiveエンティティを書くときに、ユーザーが不本意ながらも対処することに慣れてしまった、いくつかの厄介事があります:

  • IDロジックの重複:ほとんどのエンティティにはIDが必要ですが、モデルとはあまり関係がないため、ほとんどの人はIDの設定方法を気にしません。

  • ダサいゲッターとセッター:Javaは言語でプロパティをサポートしていないので、フィールドに対して読み書きを行わなかったとしてもフィールドを作成し、そのフィールドのためにゲッターとセッターを生成しなければなりません。

  • オブジェクト指向アーキテクチャの通常のオブジェクトでは、ステートとメソッドが同じクラスにないことはあり得ないのに、伝統的なEEパターンでは、エンティティの定義(モデル)とそれに対する操作(DAOやリポジトリ)を分けることが推奨されており、実際にはステートとその操作を不自然に分ける必要があります。さらに、エンティティごとに2つのクラスが必要になり、エンティティの操作を行う必要があるDAOやRepositoryをインジェクションする必要があるため、編集フローが崩れ、書いているコードから抜けてインジェクションポイントを設定してから戻って使用しなければなりません。

  • Hibernateのクエリは非常に強力ですが、一般的な操作には冗長すぎるため、すべての部分が必要ない場合でもクエリを書く必要があります。

  • Hibernateは非常に汎用性が高いのですが、モデルの使用量の9割を占めるような些細な操作をしても些細にはなりません。

Panacheでは、これらの問題に対して、定見に基づいたアプローチをとりました:

  • エンティティは PanacheEntity を拡張するようにしてください: 自動生成されるIDフィールドがあります。カスタムID戦略が必要な場合は代わりに PanacheEntityBase を拡張するとIDを自分で処理することができます。

  • パブリックフィールドを使ってください。無駄なゲッターとセッターを無くせます。フードの下では、不足しているすべてのゲッターとセッターを生成し、これらのフィールドへのすべてのアクセスを、アクセサ・メソッドを使用するように書き換えます。この方法では、必要なときに 便利な アクセサを書くことができ、エンティティ・ユーザーがフィールド・アクセスを使用していても、それが使用されます。

  • アクティブレコードパターンの使用: アクティブレコードパターンでは、すべてのエンティティロジックをエンティティクラスのスタティックメソッドに置き、DAOを作りません。エンティティスーパークラスには、非常に便利なスタティックメソッドがたくさん用意されていますし、エンティティクラスに独自のメソッドを追加することもできます。 Person ユーザーは、 Person と入力するだけで、すべての操作を一か所で完了させることができます。

  • Person.find("order by name")Person.find("name = ?1 and status = ?2", "stef", Status.Alive) 、さらには Person.find("name", "stef") のように、必要のない部分を書かないようにしましょう。

以上、Panacheを使えば、Hibernate Reactiveがこれほどまでにすっきりするのかということでした。

外部プロジェクトや jar でエンティティーを定義する

Hibernate Reactive with Panache relies on compile-time bytecode enhancements to your entities. If you define your entities in the same project where you build your Quarkus application, everything will work fine.

エンティティーが外部のプロジェクトやジャーから来ている場合は、空の META-INF/beans.xml ファイルを追加することで、jarがQuarkusアプリケーションライブラリのように扱われるようにすることができます。

これにより、Quarkusは、エンティティが現在のプロジェクトの内部にあるかのようにインデックスを作成し、バイトコード強化をすることができます。

関連コンテンツ