シンプルなHibernate Reactive with Panache
Hibernate Reactive は唯一のリアクティブJPA実装であり、オブジェクトリレーショナルマッパーの全機能を提供し、リアクティブドライバを介してデータベースにアクセスすることができます。複雑なマッピングを可能にするだけでなく、単純で一般的なマッピングもおろそかにしていません。Panacheを使用したHibernate Reactiveは、Quarkusでエンティティを簡単に楽しく書けるようにすることに重点を置いています。
最初に:例
Panacheでは、HibernateのReactiveエンティティをこのように書けるようにしています:
@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アノテーションプロセッサーを追加する必要があります: pom.xml
build.gradle
|
PanacheによるHibernate Reactiveのセットアップと設定
始めるには
-
application.propertiesに設定を追加します。 -
エンティティに
@Entityアノテーションを付けます -
エンティティが
PanacheEntityを拡張するようにする(リポジトリパターンを使用している場合は非必須です)
すべての設定は、 Hibernateセットアップガイドを確認してください。
pom.xml で、以下の依存関係を追加します:
-
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;
}
すべてのJPAのカラムアノテーションをパブリックフィールドにつけることができます。永続化されないフィールドが必要な場合は、 @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:リポジトリパターンの使用
エンティティの定義
リポジトリパターンを使用する場合、エンティティーを通常のJPAエンティティーとして定義することができます。
@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 で始まらない場合は、以下の追加の形式をサポートしています:
-
order by …はfrom EntityName order by …に展開されます -
<singleColumnName>(およびシングルパラメータ はfrom EntityName where <singleColumnName> = ?に展開されます -
<query>はfrom EntityName where <query>に展開されます
更新クエリが update で始まらない場合は、以下の追加の形式をサポートしています:
-
from EntityName …はupdate from EntityName …に展開されます -
set? <singleColumnName>(およびシングルパラメータ) はupdate from EntityName set <singleColumnName> = ?に展開されます -
set? <update-query>はupdate from EntityName set <update-query>に展開されます
削除クエリーが delete で始まらない場合は、以下の追加の形式をサポートしています:
-
from EntityName …はdelete from EntityName …に展開されます -
<singleColumnName>(およびシングルパラメータ)はdelete from EntityName where <singleColumnName> = ?に展開されます -
<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);
}
}
|
名前付きクエリーは、JPAのエンティティクラス(Panacheのエンティティクラスやリポジトリのパラメタライズドタイプ)の内部、またはそのスーパークラスのいずれかでしか定義できません。 |
クエリパラメーター
以下のように、インデックス(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射影(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プロパティから読み込まれます。 |
また、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 の 例えば、このような場合、失敗します:
|
トランザクション
データベースを変更するメソッド (例: entity.persist() ) は必ずトランザクション内で行うようにしてください。CDI Beanの機能 @ReactiveTransactional アノテーションを使うことでそのメソッドをトランザクションの境界にすることができます。あるいは、 Panache.withTransaction() を使用しても同様の効果が得られます。REST エンドポイントコントローラーのように、アプリケーションのエントリーポイントの境界でこれを行うことをお勧めします。
トランザクションにHibernate Reactiveで @Transactional を使用することはできません。 @ReactiveTransactional を使用する必要があり、アノテーションされたメソッドはノンブロッキングにするために Uni を返さなければなりません。そうしないと、 VertxThread 以外のスレッドから呼び出される必要が出てきて、ブロッキングになります。
|
JPAはエンティティに加えた変更をバッチ処理し、トランザクションの最後やクエリーの前に変更を送信します(フラッシュと呼びます)。これは効率的であるため、通常は良いことです。しかし、楽観ロックの失敗をチェックしたり、オブジェクトの検証をすぐに行ったり、一般的にはすぐにフィードバックを得たい場合には、 entity.flush() を呼び出して強制的にフラッシュ操作を行うか、あるいは entity.persistAndFlush() を使用して単一のメソッド呼び出しの中で行うことができます。これによりJPAが変更をデータベースに送信する際に発生する可能性のある PersistenceException をキャッチすることができます。ただし、これはあまり効率が良くないので悪用しないでください。また、トランザクションはまだコミットされていないはずです。
ここでは PersistenceException が発生した場合に特定の動作を行えるようにするための flush メソッドの使用例を示します:
@ReactiveTransactional
public Uni<Void> create(Person person){
//Here I 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;
});
}
@ReactiveTransactional アノテーションはテストにも有効です。これは、テスト中に行われた変更がデータベースに反映されることを意味します。テスト終了時に変更をロールバックしたい場合は、 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> {
//...
}
モック
アクティブレコードパターンの使用
アクティブレコードパターンを使用している場合、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();
}
}
モッキングテストはこのように書くことができます。
@QuarkusTest
public class PanacheFunctionalityTest {
@Test
public void testPanacheMocking() {
PanacheMock.mock(Person.class);
// Mocked classes always return a default value
Assertions.assertEquals(0, Person.count().await().indefinitely());
// Now let's specify the return value
Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(23l));
Assertions.assertEquals(23, Person.count().await().indefinitely());
// Now let's change the return value
Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(42l));
Assertions.assertEquals(42, Person.count().await().indefinitely());
// Now let's call the original method
Mockito.when(Person.count()).thenCallRealMethod();
Assertions.assertEquals(0, Person.count().await().indefinitely());
// Check that we called it 4 times
PanacheMock.verify(Person.class, Mockito.times(4)).count();(1)
// Mock only with specific parameters
Person p = new Person();
Mockito.when(Person.findById(12l)).thenReturn(Uni.createFrom().item(p));
Assertions.assertSame(p, Person.findById(12l).await().indefinitely());
Assertions.assertNull(Person.findById(42l).await().indefinitely());
// Mock throwing
Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException());
try {
Person.findById(12l);
Assertions.fail();
} catch (WebApplicationException x) {
}
// We can even mock your custom methods
Mockito.when(Person.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList()));
Assertions.assertTrue(Person.findOrdered().await().indefinitely().isEmpty());
PanacheMock.verify(Person.class).findOrdered();
PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any());
PanacheMock.verifyNoMoreInteractions(Person.class);
}
}
| 1 | verify と do* のメソッドは Mockito ではなく PanacheMock で呼び出すようにしてください。そうしないとどのモックオブジェクトを渡せばいいのかわからなくなってしまいます。 |
Mutiny.Session とエンティティインスタンスのメソッドのモック化
persist() のようなエンティティインスタンスのメソッドをモックにする必要がある場合は、Hibernate Reactive Mutiny.Session オブジェクトをモック化することで実現できます:
@QuarkusTest
public class PanacheMockingTest {
@InjectMock
Mutiny.Session session;
@Test
public void testPanacheSessionMocking() {
Person p = new Person();
// mocked via Mutiny.Session mocking
p.persist().await().indefinitely();
Assertions.assertNull(p.id);
Mockito.verify(session, Mockito.times(1)).persist(Mockito.any());
}
}
リポジトリパターンの使用
リポジトリパターンを使用している場合は、 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();
}
}
モッキングテストはこのように書くことができます。
@QuarkusTest
public class PanacheFunctionalityTest {
@InjectMock
PersonRepository personRepository;
@Test
public void testPanacheRepositoryMocking() throws Throwable {
// Mocked classes always return a default value
Assertions.assertEquals(0, mockablePersonRepository.count().await().indefinitely());
// Now let's specify the return value
Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(23l));
Assertions.assertEquals(23, mockablePersonRepository.count().await().indefinitely());
// Now let's change the return value
Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(42l));
Assertions.assertEquals(42, mockablePersonRepository.count().await().indefinitely());
// Now let's call the original method
Mockito.when(mockablePersonRepository.count()).thenCallRealMethod();
Assertions.assertEquals(0, mockablePersonRepository.count().await().indefinitely());
// Check that we called it 4 times
Mockito.verify(mockablePersonRepository, Mockito.times(4)).count();
// Mock only with specific parameters
Person p = new Person();
Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(Uni.createFrom().item(p));
Assertions.assertSame(p, mockablePersonRepository.findById(12l).await().indefinitely());
Assertions.assertNull(mockablePersonRepository.findById(42l).await().indefinitely());
// Mock throwing
Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException());
try {
mockablePersonRepository.findById(12l);
Assertions.fail();
} catch (WebApplicationException x) {
}
// We can even mock your custom methods
Mockito.when(mockablePersonRepository.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList()));
Assertions.assertTrue(mockablePersonRepository.findOrdered().await().indefinitely().isEmpty());
Mockito.verify(mockablePersonRepository).findOrdered();
Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any());
Mockito.verify(mockablePersonRepository).persist(Mockito.<Person> any());
Mockito.verifyNoMoreInteractions(mockablePersonRepository);
}
}
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は、コンパイル時のエンティティに対するバイトコード拡張に依存しています。
この機能は、マーカーファイル META-INF/panache-archive.marker の存在によって Panache エンティティー の存在するアーカイブ(および Panache エンティティーの消費者) を識別しようとします 。Panache にはアノテーション プロセッサーが含まれており、 (間接的であっても) Panache に依存しているアーカイヴでこのファイルを自動的に作成します。アノテーションプロセッサーを無効にしている場合は、場合によってはこのファイルを手動で作成する必要があるかもしれません。
jpa-modelgenアノテーションプロセッサーをインクルードすると、デフォルトでPanacheアノテーションプロセッサが除外されます。この場合はマーカーファイルを自分で作成するか、以下のように quarkus-panache-common を追加する必要があります:
|
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
</annotationProcessorPath>
<annotationProcessorPath>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-common</artifactId>
<version>${quarkus.platform.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</plugin>