Simplified 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");
}
}
コードがどれだけコンパクトで読みやすくなっているかお気づきですか?面白いと思いませんか?読んでみてください。
the list() method might be surprising at first. It takes fragments of HQL (JP-QL) queries and contextualizes the rest. That makes for very concise but yet readable code.
|
what was described above is essentially the active record pattern, sometimes just called the entity pattern.
Hibernate with Panache also allows for the use of the more classical repository pattern via PanacheRepository.
|
ソリューション
次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。
git clone https://github.com/quarkusio/quarkus-quickstarts.git で Git リポジトリーをクローンします。または、https://github.com/quarkusio/quarkus-quickstarts/archive/2.16.zip[アーカイブ] をダウンロードします。
ソリューションは hibernate-reactive-panache-quickstart ディレクトリ にあります。
|
プロジェクトがすでに他のアノテーションプロセッサーを使用するように設定されている場合、追加でPanacheアノテーションプロセッサーを追加する必要があります: pom.xml
build.gradle
|
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;
}
すべての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;
}
}
If you don’t want to bother defining getters/setters for your entities, you can make them extend PanacheEntityBase and
Quarkus will generate them for you. You can even extend PanacheEntity and take advantage of the default ID it provides.
|
リポジトリの定義
リポジトリを使用する場合、 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);
| The rest of the documentation show usages based on the active record pattern only, but keep in mind that they can be performed with the repository pattern as well. The repository pattern examples have been omitted for brevity. |
高度なクエリー
ページング
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>に展開されます
| You can also write your queries in plain 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 エンドポイントコントローラーのように、アプリケーションのエントリーポイントの境界でこれを行うことをお勧めします。
You cannot use @Transactional with Hibernate Reactive for your transactions: you must use @ReactiveTransactional,
and your annotated method must return a Uni to be non-blocking. Otherwise, it needs be called from a non-VertxThread thread
and will become blocking.
|
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 で呼び出すようにしてください。そうしないとどのモックオブジェクトを渡せばいいのかわからなくなってしまいます。 |
Mocking Mutiny.Session and entity instance methods
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 に依存しているアーカイヴでこのファイルを自動的に作成します。アノテーションプロセッサーを無効にしている場合は、場合によってはこのファイルを手動で作成する必要があるかもしれません。
If you include the jpa-modelgen annotation processor this will exclude the Panache
annotation processor by default. If you do this you should either create the marker file
yourself, or add the quarkus-panache-common as well, as shown below:
|
<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>