PanacheでシンプルになったHibernate Reactive
Hibernate Reactiveは 、唯一のリアクティブ Jakarta Persistence(旧JPA)実装で、リアクティブドライバでデータベースにアクセスできるObject Relational Mapperの全機能を提供します。複雑なマッピングを可能にしますが、単純で一般的なマッピングを容易なものにするわけではありません。Hibernate Reactive with Panacheは、Quarkusでエンティティを簡単に、楽しく書けるようにすることに重点を置いています。
Hibernate Reactive は、Hibernate ORM を置き換えるものでも、次世代の Hibernate ORM でもありません。 これは、高い同時実行性が必要なリアクティブなユースケース向けに調整された別のスタックです。 さらに、デフォルトの REST レイヤーである Quarkus REST (旧称 RESTEasy Reactive) を使用する場合、Hibernate Reactive を使用する必要はありません。 Quarkus REST を Hibernate ORM と併用することはまったく問題ありません。 高い同時実行性が必要ない場合、またはリアクティブパラダイムに慣れていない場合は、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
, … )
例えば:
<!-- 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>
// 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();
範囲とページを混在させることはできません。範囲を使用した場合、現在のページを持っていることに依存するすべてのメソッドは |
ソート
クエリー文字列を受け付けるすべてのメソッドは、以下の簡略化されたクエリー形式も受け付けます:
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 …]
というように最後にオプションの要素を持つという形式になっています。
選択クエリーが from
、 select
、または with
で始まっていない場合は、次の追加形式がサポートされます。
-
order by …
はfrom EntityName order by …
に展開されます -
<singleAttribute>` (および単一のパラメーター) は
from EntityName where <singleAttribute> = ?
に展開されます -
where <query>
はfrom EntityName where <query>
に展開されます -
<query>
はfrom EntityName where <query>
に展開されます
更新クエリーが update
で始まらない場合は、以下の追加の形式をサポートしています:
-
from EntityName …` は
update EntityName …
に展開されます -
set? <singleAttribute>
(および単一のパラメーター) はupdate EntityName set <singleAttribute> = ?
に展開されます -
set? <update-query>
はupdate EntityName set <update-query>
に展開されます
削除クエリーが delete
で始まらない場合は、以下の追加の形式をサポートしています:
-
from EntityName …
はdelete from EntityName …
に展開されます -
<singleAttribute>
(および単一のパラメーター) は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 エンティティークラス内で、またはそのスーパークラスの 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 によって使用されます。このコンストラクタはクラス内の唯一のコンストラクタであり、パラメータとしてクラスのすべての属性を持つ必要があります。 |
|
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プロパティから読み込まれます。 |
ネストされたクラスを持つクラスにエンティティーを射影する場合は、それらのネストされたクラスで @NestedProjectedClass
アノテーションを使用できます。
@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 | このアノテーションは、 @Embedded エンティティーまたは @ManyToOne 、 @OneToOne リレーションを射影する場合に使用できます。
@OneToMany または @ManyToMany 関係はサポートされていません。 |
また、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 例えば、このような場合、失敗します。
|
セッションとトランザクション
まず、Panache エンティティーのメソッドのほとんどは、リアクティブ Mutiny.Session
のスコープ内で呼び出す必要があります。
場合によっては、セッションは要求に応じて自動的に開かれます。
たとえば、 quarkus-rest
エクステンションを含むアプリケーションの Jakarta REST リソースメソッドで Panache エンティティーメソッドが呼び出される場合です。
その他の場合、セッションが開かれていることを確認するための宣言的な方法とプログラム的な方法があります。
Uni
を返す CDI ビジネスメソッドに @WithSession
アノテーションを付けることができます。
メソッドはインターセプトされ、返された Uni
はリアクティブセッションのスコープ内でトリガーされます。
または、 Panache.withSession()
メソッドを使用して同じ効果を得ることもできます。
Panache エンティティーはブロッキングスレッドから使用できないことに注意してください。Quarkus のリアクティブ原則の基礎を説明する リアクティブ入門 ガイドも参照してください。 |
また、データベースを変更するメソッドや複数のクエリー (例: entity.persist()
) を伴うメソッドをトランザクション内でラップしてください。
Uni
を返す CDI ビジネスメソッドに @WithTransaction
アノテーションを付けることができます。
メソッドはインターセプトされ、返された Uni
はトランザクション境界内でトリガーされます。
または、 Panache.withTransaction()
メソッドを使用して同じ効果を得ることもできます。
Hibernate Reactive で @Transactional アノテーションをトランザクションに使用することはできません。 @WithTransaction を使用する必要があります。また、アノテーションが付けられたメソッドが非ブロッキングとなるように Uni を返す必要があります。
|
Hibernate Reactive は、エンティティーに加えられた変更をバッチ処理し、トランザクションの終了時またはクエリーの前に変更を送信します (これはフラッシュと呼ばれます)。
これは通常、より効率的であるため良いことです。
しかし、楽観的なロックの失敗をチェックしたり、オブジェクト検証をすぐに実行したり、一般的に即時のフィードバックを取得する場合は、 entity.flush()
を呼び出してフラッシュ操作を強制したり、 entity.persistAndFlush()
を使用して単一のメソッド呼び出しにすることができます。これにより、Hibernate Reactive がデータベースに変更を送信したときに発生する可能性のある PersistenceException
をキャッチできます。
ただし、これは効率が悪いので乱用しないでください。
また、トランザクションは依然としてコミットされる必要があります。
ここでは 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;
});
}
@WithTransaction
アノテーションもテストに使用できます。
これは、テスト中に行われた変更がデータベースに伝播されることを意味します。
テスト終了時に変更をロールバックする場合は、 io.quarkus.test.TestReactiveTransaction
アノテーションを使用できます。
これにより、トランザクション内でテストメソッドが実行されますが、テストメソッドが完了するとロールバックされ、データベースの変更が元に戻ります。
ロック管理
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 ガイドに記載されています。
さらに、 quarkus-test-hibernate-reactive-panache
依存関係は、 @RunOnVertxContext
でアノテーションが付けられたテストメソッドのメソッドパラメーターとして注入できる io.quarkus.test.hibernate.reactive.panache.TransactionalUniAsserter
を提供します。
TransactionalUniAsserter
は、各アサートメソッドを個別のリアクティブトランザクション内にラップする io.quarkus.test.vertx.UniAsserterInterceptor
です。
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 | 1 つ目のリアクティブトランザクションは、エンティティーを永続化するために使用されます。 |
2 | 2 つ目のリアクティブトランザクションは、エンティティーをカウントするために使用されます。 |
3 | 3 つ目のリアクティブトランザクションは、すべてのエンティティーを削除するために使用されます。 |
もちろん、カスタムの UniAsserterInterceptor
を定義して、注入された UniAsserter
をラップし、動作をカスタマイズすることもできます。
モック
アクティブレコードパターンの使用
アクティブレコードパターンを使用している場合、Mockitoは静的メソッドのモックをサポートしていないため、直接使用することはできませんが、 quarkus-panache-mock
モジュールを使用することで、Mockitoを使用して、あなた自身のメソッドを含む、提供されたすべての静的メソッドをモックすることができます。
この依存関係をビルドファイルに追加してください:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-mock</artifactId>
<scope>test</scope>
</dependency>
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 | 注入された UniAsserter 引数はアサーションを行うために使用されます。 |
3 | verify と do* のメソッドは Mockito ではなく PanacheMock で呼び出すようにしてください。そうしないとどのモックオブジェクトを渡せばいいのかわからなくなってしまいます。 |
リポジトリパターンの使用
リポジトリパターンを使用している場合は、 quarkus-junit5-mockito
モジュールを使用して、Mockito を直接使用することができます。これにより、ビーンのモッキングが非常に簡単になります。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
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 | 注入された UniAsserter 引数はアサーションを行うために使用されます。 |
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 は、コンパイル時のバイトコード拡張によってエンティティーを拡張します。 Quarkus アプリケーションをビルドするのと同じプロジェクトでエンティティーを定義すれば、すべて正常に動作します。
エンティティーが外部のプロジェクトやジャーから来ている場合は、空の META-INF/beans.xml
ファイルを追加することで、jarがQuarkusアプリケーションライブラリのように扱われるようにすることができます。
これにより、Quarkusは、エンティティが現在のプロジェクトの内部にあるかのようにインデックスを作成し、バイトコード強化をすることができます。