シンプルになったHibernate ORM with Panache
Hibernate ORMは、デファクトのJakarta Persistence(旧称JPA)実装であり、Object Relational Mapperの全機能を提供するものです。複雑なマッピングを可能にしますが、単純で一般的なマッピングを些細なものにするわけではありません。Hibernate ORM with Panacheは、Quarkusでエンティティを簡単に、楽しく書けるようにすることに重点を置いています。
最初に:一例
Panacheでやっていることは、Hibernate ORMエンティティをこのように書けるようにすることです:
package org.acme;
public enum Status {
Alive,
Deceased
}
package org.acme;
import java.time.LocalDate;
import java.util.List;
import jakarta.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
@Entity
public class Person extends PanacheEntity {
public String name;
public LocalDate birth;
public Status status;
public static Person findByName(String name){
return find("name", name).firstResult();
}
public static List<Person> findAlive(){
return list("status", Status.Alive);
}
public static void deleteStefs(){
delete("name", "Stef");
}
}
コードがどれだけコンパクトで読みやすくなっているかお気づきですか?面白いと思いませんか?読んでみてください!
list() メソッドには最初は驚くかもしれません。これは HQL (JP-QL) クエリーの断片を取り、残りの部分をコンテキストに依存させるようにします。これにより非常に簡潔でありながら読みやすいコードになっています。
|
上記で説明したものは、基本的には アクティブレコードパターン であり、エンティティパターンと呼ばれることもあります。 Hibernate with Panache は、 PanacheRepository を使用して、より古典的な リポジトリパターン を使用することもできます。
|
ソリューション
次のセクションで紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。
Gitレポジトリをクローンするか git clone https://github.com/quarkusio/quarkus-quickstarts.git
、 アーカイブ をダウンロードします。
ソリューションは hibernate-orm-panache-quickstart
ディレクトリ にあります。
プロジェクトがすでに他のアノテーションプロセッサーを使用するように設定されている場合、追加でPanacheアノテーションプロセッサーを追加する必要があります: pom.xml
build.gradle
|
PanacheによるHibernate ORMのセットアップと設定
始めるには:
-
application.properties
で設定を追加します -
エンティティに
@Entity
アノテーションを付けます -
エンティティが
PanacheEntity
を拡張するようにする(リポジトリパターンを使用している場合は非必須です)
すべての設定は、Hibernateセットアップガイドを確認してください。
ビルドファイルに、以下の依存関係を追加します:
-
Hibernate ORM with Panache エクステンション
-
お使いの JDBC ドライバーエクステンション (
quarkus-jdbc-postgresql
,quarkus-jdbc-h2
,quarkus-jdbc-mariadb
, …)
<!-- Hibernate ORM specific dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<!-- JDBC driver dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
// Hibernate ORM specific dependencies
implementation("io.quarkus:quarkus-hibernate-orm-panache")
// JDBC driver dependencies
implementation("io.quarkus:quarkus-jdbc-postgresql")
次に、 application.properties
で関連する設定プロパティを追加します。
# configure your datasource
quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = sarah
quarkus.datasource.password = connor
quarkus.datasource.jdbc.url = jdbc: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
アノテーションを付け、列をパブリック フィールドとして追加します:
package org.acme;
import java.time.LocalDate;
import java.util.List;
import jakarta.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
@Entity
public class Person extends PanacheEntity {
public String name;
public LocalDate birth;
public Status status;
}
You can put all your Jakarta Persistence column annotations on the public fields. If you need a field to not be persisted, use the @Transient
annotation on it. If you need to write accessors, you can:
package org.acme;
import java.time.LocalDate;
import java.util.List;
import jakarta.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
@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()
アクセサが呼び出されます。フィールドの書き込みやセッターについても同様です。これにより、すべてのフィールドの呼び出しが、対応するゲッター/セッターの呼び出しに置き換えられるため、実行時に適切なカプセル化が可能になります。
最も使うことの多い操作
エンティティを記述したら、ここでは実行できる最も一般的な操作を紹介します:
import java.time.LocalDate;
import java.time.Month;
import java.util.List;
import java.util.Optional;
// creating a person
Person person = new Person();
person.name = "Stef";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;
// persist it
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
person.delete();
}
// getting a list of all Person entities
List<Person> allPersons = Person.listAll();
// finding a specific person by ID
person = Person.findById(personId);
// finding a specific person by ID via an Optional
Optional<Person> optional = Person.findByIdOptional(personId);
person = optional.orElseThrow(() -> new NotFoundException());
// finding all living persons
List<Person> livingPersons = Person.list("status", Status.Alive);
// counting all persons
long countAll = Person.count();
// counting all living persons
long countAlive = Person.count("status", Status.Alive);
// delete all living persons
Person.delete("status", Status.Alive);
// delete all persons
Person.deleteAll();
// delete by id
boolean deleted = Person.deleteById(personId);
// set the name of all living persons to 'Mortal'
Person.update("name = 'Mortal' where status = ?1", Status.Alive);
すべての list
メソッドには同等の stream
バージョンがあります。
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
try (Stream<Person> persons = Person.streamAll()) {
List<String> namesButEmmanuels = persons
.map(p -> p.name.toLowerCase() )
.filter( n -> ! "emmanuel".equals(n) )
.collect(Collectors.toList());
}
stream のメソッドは動作にトランザクションを必要とします。 また、これらのメソッドは I/O 操作を行うため close() メソッドや try-with-resource を介して基礎となる ResultSet を閉じなければなりません。そうしないと、Agroal からの警告が表示され、基礎となる ResultSet を閉じてくれます。
|
エンティティメソッドの追加
エンティティに対するカスタムクエリーをエンティティの中に追加できます。そうすることで、あなたやあなたの同僚が簡単に見つけることができるようになり、クエリーは操作するオブジェクトと一緒に配置されます。エンティティクラスにスタティックメソッドとして追加するのがPanache Active Recordのやり方です。
package org.acme;
import java.time.LocalDate;
import java.util.List;
import jakarta.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
@Entity
public class Person extends PanacheEntity {
public String name;
public LocalDate birth;
public Status status;
public static Person findByName(String name){
return find("name", name).firstResult();
}
public static List<Person> findAlive(){
return list("status", Status.Alive);
}
public static void deleteStefs(){
delete("name", "Stef");
}
}
解決策2:リポジトリパターンを使用する
エンティティの定義
リポジトリパターンを使用する場合、エンティティを通常のJakarta Persistenceエンティティとして定義することができます。
package org.acme;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.time.LocalDate;
@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
を実装することでアクティブレコードパターンとまったく同じ便利なメソッドをリポジトリにインジェクションできます:
package org.acme;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {
// put your custom logic here as instance methods
public Person findByName(String name){
return find("name", name).firstResult();
}
public List<Person> findAlive(){
return list("status", Status.Alive);
}
public void deleteStefs(){
delete("name", "Stef");
}
}
PanacheEntityBase
で定義されている操作はすべてリポジトリ上で利用可能なので、これを使用することはアクティブレコードパターンを使用するのと全く同じですが、その機能をインジェクションする必要があります:
import jakarta.inject.Inject;
@Inject
PersonRepository personRepository;
@GET
public long count(){
return personRepository.count();
}
最も使うことの多い操作
リポジトリを記述したら、ここでは実行できる最も一般的な操作を紹介します:
import java.time.LocalDate;
import java.time.Month;
import java.util.List;
import java.util.Optional;
// creating a person
Person person = new Person();
person.setName("Stef");
person.setBirth(LocalDate.of(1910, Month.FEBRUARY, 1));
person.setStatus(Status.Alive);
// persist it
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
personRepository.delete(person);
}
// getting a list of all Person entities
List<Person> allPersons = personRepository.listAll();
// finding a specific person by ID
person = personRepository.findById(personId);
// finding a specific person by ID via an Optional
Optional<Person> optional = personRepository.findByIdOptional(personId);
person = optional.orElseThrow(() -> new NotFoundException());
// finding all living persons
List<Person> livingPersons = personRepository.list("status", Status.Alive);
// counting all persons
long countAll = personRepository.count();
// counting all living persons
long countAlive = personRepository.count("status", Status.Alive);
// delete all living persons
personRepository.delete("status", Status.Alive);
// delete all persons
personRepository.deleteAll();
// delete by id
boolean deleted = personRepository.deleteById(personId);
// set the name of all living persons to 'Mortal'
personRepository.update("name = 'Mortal' where status = ?1", Status.Alive);
すべての list
メソッドには同等の stream
バージョンがあります。
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Stream<Person> persons = personRepository.streamAll();
List<String> namesButEmmanuels = persons
.map(p -> p.name.toLowerCase() )
.filter( n -> ! "emmanuel".equals(n) )
.collect(Collectors.toList());
stream メソッドを使うにはトランザクションが必要です。
|
残りのドキュメントではアクティブレコードパターンに基づく使用法のみを示していますが、リポジトリパターンでも実行できることを覚えておいてください。リポジトリパターンの例は簡潔にするために省略しています。 |
Jakarta RESTリソースの書き方
まず、Jakarta RESTエンドポイントを有効にするために、RESTEasy Reactiveエクステンションのいずれかを組み込みます。例えば、Jakarta RESTとJSONをサポートするために、 io.quarkus:quarkus-resteasy-reactive-jackson
の依存関係を追加します。
そして、次のようなリソースを作成することで、Personエンティティの作成/読み取り/更新/削除が可能になります:
package org.acme;
import java.net.URI;
import java.util.List;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path("/persons")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PersonResource {
@GET
public List<Person> list() {
return Person.listAll();
}
@GET
@Path("/{id}")
public Person get(Long id) {
return Person.findById(id);
}
@POST
@Transactional
public Response create(Person person) {
person.persist();
return Response.created(URI.create("/persons/" + person.id)).build();
}
@PUT
@Path("/{id}")
@Transactional
public Person update(Long id, Person person) {
Person entity = Person.findById(id);
if(entity == null) {
throw new NotFoundException();
}
// map all fields from the person parameter to the existing entity
entity.name = person.name;
return entity;
}
@DELETE
@Path("/{id}")
@Transactional
public void delete(Long id) {
Person entity = Person.findById(id);
if(entity == null) {
throw new NotFoundException();
}
entity.delete();
}
@GET
@Path("/search/{name}")
public Person search(String name) {
return Person.findByName(name);
}
@GET
@Path("/count")
public Long count() {
return Person.count();
}
}
データベースを変更する操作には @Transactional アノテーションを使用するように注意してください。わかりやすくするために、クラスレベルでアノテーションを追加することもできます。
|
To make it easier to showcase some capabilities of Hibernate ORM with Panache on Quarkus with Dev mode, some test data should be inserted into the database by adding the following content to a new file named src/main/resources/import.sql:
INSERT INTO person (id, birth, name, status) VALUES (1, '1995-09-12', 'Emily Brown', 0);
ALTER SEQUENCE person_seq RESTART WITH 2;
If you would like to initialize the DB when you start the Quarkus app in your production environment, add quarkus.hibernate-orm.database.generation=drop-and-create to the Quarkus startup options in addition to import.sql .
|
After that, you can see the people list and add new person as followings:
$ curl -w "\n" http://localhost:8080/persons
[{"id":1,"name":"Emily Brown","birth":"1995-09-12","status":"Alive"}]
$ curl -X POST -H "Content-Type: application/json" -d '{"name" : "William Davis" , "birth" : "1988-07-04", "status" : "Alive"}' http://localhost:8080/persons
$ curl -w "\n" http://localhost:8080/persons
[{"id":1,"name":"Emily Brown","birth":"1995-09-12","status":"Alive"}, {"id":2,"name":"William Davis","birth":"1988-07-04","status":"Alive"}]
PersonオブジェクトがPerson<1>と表示される場合は、オブジェクトが変換されていないことになります。この場合、 pom.xml の依存関係 quarkus-resteasy-reactive-jackson を追加してください。
|
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
高度なクエリー
ページング
list
および stream
メソッドは、テーブルに含まれるデータセットが十分に小さい場合にのみ使用してください。より大きなデータセットの場合は、同等の find
メソッドを使用して、ページングが可能な PanacheQuery
を返すことができます:
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.panache.common.Page;
import java.util.List;
// 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
List<Person> firstPage = livingPersons.list();
// get the second page
List<Person> secondPage = livingPersons.nextPage().list();
// get page 7
List<Person> page7 = livingPersons.page(Page.of(7, 25)).list();
// get the number of pages
int numberOfPages = livingPersons.pageCount();
// get the total number of entities returned by this query without paging
long count = livingPersons.count();
// and you can chain methods of course
return Person.find("status", Status.Alive)
.page(Page.ofSize(25))
.nextPage()
.stream()
PanacheQuery
型には、ページングや返されたストリームを処理するための他の多くのメソッドがあります。
ページの代わりにレンジを使用
PanacheQuery
では、レンジベースのクエリーも使用できます。
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import java.util.List;
// 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
List<Person> firstRange = livingPersons.list();
// to get the next range, you need to call range again
List<Person> secondRange = livingPersons.range(25, 49).list();
レンジとページを混在させることはできません。レンジを使用した場合、現在のページを持っていることに依存するすべてのメソッドは |
ソート
クエリー文字列を受け付けるすべてのメソッドは、以下の簡略化されたクエリー形式も受け付けます:
List<Person> persons = Person.list("order by name,birth");
しかし、これらのメソッドには、オプションで Sort
というパラメータが用意されており、これによってソートの抽象化が可能になります:
import io.quarkus.panache.common.Sort;
List<Person> persons = Person.list(Sort.by("name").and("birth"));
// and with more restrictions
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"
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 …
which will expand toupdate EntityName …
-
set? <singleColumnName>
(and single parameter) which will expand toupdate EntityName set <singleColumnName> = ?
-
set? <update-query>
will expand toupdate 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 Person set name = 'Mortal' where status = ?", Status.Alive);
名前付きクエリー
名前付きのクエリーは、その名前の前に「#」文字を付けることで、(簡易)HQLクエリーの代わりに参照することができます。また、名前付きのクエリーは、カウント、更新、削除のクエリーにも使用できます。
package org.acme;
import java.time.LocalDate;
import jakarta.persistence.Entity;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.panache.common.Parameters;
@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 Person findByName(String name){
return find("#Person.getByName", name).firstResult();
}
public static long countByStatus(Status status) {
return count("#Person.countByStatus", Parameters.with("status", status).map());
}
public static long updateStatusById(Status status, long id) {
return update("#Person.updateStatusById", Parameters.with("status", status).and("id", id));
}
public static long deleteById(long id) {
return delete("#Person.deleteById", id);
}
}
Named queries can only be defined inside your Jakarta Persistence entity classes (being the Panache entity class, or the repository parameterized type), or on one of its super classes. |
クエリーパラメーター
以下のように、インデックス(1ベース)でクエリーパラメーターを渡すことができます:
Person.find("name = ?1 and status = ?2", "stef", Status.Alive);
または、 Map
を使った名前で:
import java.util.HashMap;
import java.util.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 句を参照してください。
射影クラスは、すべての属性を含むコンストラクタを持つ必要があります。このコンストラクタは、エンティティクラスを使用する代わりに、射影のDTOをインスタンス化するために使用されます。このクラスは、すべてのクラス属性をパラメータとして持つコンストラクタを持つ必要があります。
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.quarkus.hibernate.orm.panache.PanacheQuery;
@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 によって使用されます。このコンストラクタはクラス内の唯一のコンストラクタであり、パラメータとしてクラスのすべての属性を持つ必要があります。 |
|
Java 17+を実行する場合、レコード型は投影クラスと相性が良いです。 |
DTO射影のオブジェクトから参照されるエンティティのフィールドがある場合、 @ProjectedFieldName
アノテーションを使用してSELECT文のパスを提供することができます。
import jakarta.persistence.ManyToOne;
import io.quarkus.hibernate.orm.panache.common.ProjectedFieldName;
@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 ORM は、このコンストラクタを使用します。クエリが select 節を持つ場合、複数のコンストラクタを持てます。 |
HQL の`select new` クエリと 例えば、このような場合、失敗します:
|
複数の永続化ユニット
複数の永続化ユニットのサポートについては Hibernate ORMガイドで詳しく説明されています。
Panacheの使い方は簡単です:
-
1つのPanacheエンティティは、1つの永続化ユニットにしかアタッチできません。
-
そう考えると、Panacheはすでに、Panacheエンティティに関連する適切な
EntityManager
を透過的に見つけるために必要な導線を提供しています。
トランザクション
データベースを変更するメソッド (例: entity.persist()
) は必ずトランザクション内で行うようにしてください。CDI Beanの機能 @Transactional
アノテーションを使うことでそのメソッドをトランザクションの境界にできます。REST エンドポイントコントローラーのように、アプリケーションのエントリーポイントの境界でこれを行うことをお勧めします。
Hibernate ORM 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 ORM sends 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 メソッドの使用例を示します:
import jakarta.persistence.PersistenceException;
@Transactional
public void create(Parameter parameter){
try {
//Here I use the persistAndFlush() shorthand method on a Panache repository to persist to database then flush the changes.
return parameterRepository.persistAndFlush(parameter);
}
catch(PersistenceException pe){
LOG.error("Unable to create the parameter", pe);
//in case of error, I save it to disk
diskPersister.save(parameter);
}
}
ロック管理
Panacheは findById(Object, LockModeType)
や find().withLock(LockModeType)
を使用してエンティティ/リポジトリでデータベースロックを直接サポートします。
以下の例はアクティブレコードパターンの場合ですが、リポジトリでも同じように使用できます。
1つ目: findById()を使ってロックする。
import jakarta.persistence.LockModeType;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.GET;
public class PersonEndpoint {
@GET
@Transactional
public Person findByIdForUpdate(Long id){
Person p = Person.findById(id, LockModeType.PESSIMISTIC_WRITE);
//do something useful, the lock will be released when the transaction ends.
return person;
}
}
2つ目: find()でロックする。
import jakarta.persistence.LockModeType;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.GET;
public class PersonEndpoint {
@GET
@Transactional
public Person findByNameForUpdate(String name){
Person p = Person.find("name", name).withLock(LockModeType.PESSIMISTIC_WRITE).findOne();
//do something useful, the lock will be released when the transaction ends.
return person;
}
}
トランザクションが終了するとロックが解放されるため、ロッククエリーを呼び出すメソッドには @Transactional
アノテーションを付ける必要があります。
カスタムID
IDは微妙な問題で、誰もがフレームワークに任せることができるわけではありませんが、今回も私たちはサポートします。
PanacheEntity
の代わりに PanacheEntityBase
を拡張することで独自のID戦略を指定することができます。そのあとに好きなIDをパブリック・フィールドとして宣言するだけです:
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
@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の型を追加の型パラメーターとして指定することになります:
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class PersonRepository implements PanacheRepositoryBase<Person,Integer> {
//...
}
モック
アクティブレコードパターンの使用
アクティブレコードパターンを使用している場合、Mockitoは静的メソッドのモックをサポートしていないため、直接使用することはできませんが、 quarkus-panache-mock
モジュールを使用することで、Mockitoを使用して、あなた自身のメソッドを含む、提供されたすべての静的メソッドをモックすることができます。
この依存関係を pom.xml
に追加してください:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-mock</artifactId>
<scope>test</scope>
</dependency>
このシンプルなエンティティ:
@Entity
public class Person extends PanacheEntity {
public String name;
public static List<Person> findOrdered() {
return find("ORDER BY name").list();
}
}
モック化テストはこのように書くことができます:
import io.quarkus.panache.mock.PanacheMock;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import jakarta.ws.rs.WebApplicationException;
import java.util.Collections;
@QuarkusTest
public class PanacheFunctionalityTest {
@Test
public void testPanacheMocking() {
PanacheMock.mock(Person.class);
// Mocked classes always return a default value
Assertions.assertEquals(0, Person.count());
// Now let's specify the return value
Mockito.when(Person.count()).thenReturn(23L);
Assertions.assertEquals(23, Person.count());
// Now let's change the return value
Mockito.when(Person.count()).thenReturn(42L);
Assertions.assertEquals(42, Person.count());
// Now let's call the original method
Mockito.when(Person.count()).thenCallRealMethod();
Assertions.assertEquals(0, Person.count());
// 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(p);
Assertions.assertSame(p, Person.findById(12L));
Assertions.assertNull(Person.findById(42L));
// Mock throwing
Mockito.when(Person.findById(12L)).thenThrow(new WebApplicationException());
Assertions.assertThrows(WebApplicationException.class, () -> Person.findById(12L));
// We can even mock your custom methods
Mockito.when(Person.findOrdered()).thenReturn(Collections.emptyList());
Assertions.assertTrue(Person.findOrdered().isEmpty());
// Mocking a void method
Person.voidMethod();
// Make it throw
PanacheMock.doThrow(new RuntimeException("Stef2")).when(Person.class).voidMethod();
try {
Person.voidMethod();
Assertions.fail();
} catch (RuntimeException x) {
Assertions.assertEquals("Stef2", x.getMessage());
}
// Back to doNothing
PanacheMock.doNothing().when(Person.class).voidMethod();
Person.voidMethod();
// Make it call the real method
PanacheMock.doCallRealMethod().when(Person.class).voidMethod();
try {
Person.voidMethod();
Assertions.fail();
} catch (RuntimeException x) {
Assertions.assertEquals("void", x.getMessage());
}
PanacheMock.verify(Person.class).findOrdered();
PanacheMock.verify(Person.class, Mockito.atLeast(4)).voidMethod();
PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any());
PanacheMock.verifyNoMoreInteractions(Person.class);
}
}
1 | verify と do* のメソッドは Mockito ではなく PanacheMock で呼び出すようにしてください。そうしないとどのモックオブジェクトを渡せばいいのかわからなくなってしまいます。 |
EntityManager
、 Session
とエンティティインスタンスのメソッドのモック化
persist()
のようなエンティティインスタンスのメソッドをモックにする必要がある場合は、Hibernate ORMの Session
オブジェクトをモック化することで実現できます:
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import org.hibernate.Session;
import org.hibernate.query.Query;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusTest
public class PanacheMockingTest {
@InjectMock
Session session;
@BeforeEach
public void setup() {
Query mockQuery = Mockito.mock(Query.class);
Mockito.doNothing().when(session).persist(Mockito.any());
Mockito.when(session.createQuery(Mockito.anyString())).thenReturn(mockQuery);
Mockito.when(mockQuery.getSingleResult()).thenReturn(0l);
}
@Test
public void testPanacheMocking() {
Person p = new Person();
// mocked via EntityManager mocking
p.persist();
Assertions.assertNull(p.id);
Mockito.verify(session, Mockito.times(1)).persist(Mockito.any());
}
}
リポジトリパターンの使用
リポジトリパターンを使用している場合は、 quarkus-junit5-mockito
モジュールを使用して、Mockito を直接使用することができます。これにより、Bean のモック化が非常に簡単になります:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
このシンプルなエンティティ:
@Entity
public class Person {
@Id
@GeneratedValue
public Long id;
public String name;
}
そしてこのリポジトリ:
@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {
public List<Person> findOrdered() {
return find("ORDER BY name").list();
}
}
モック化テストはこのように書くことができます:
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import jakarta.ws.rs.WebApplicationException;
import java.util.Collections;
@QuarkusTest
public class PanacheFunctionalityTest {
@InjectMock
PersonRepository personRepository;
@Test
public void testPanacheRepositoryMocking() throws Throwable {
// Mocked classes always return a default value
Assertions.assertEquals(0, personRepository.count());
// Now let's specify the return value
Mockito.when(personRepository.count()).thenReturn(23L);
Assertions.assertEquals(23, personRepository.count());
// Now let's change the return value
Mockito.when(personRepository.count()).thenReturn(42L);
Assertions.assertEquals(42, personRepository.count());
// Now let's call the original method
Mockito.when(personRepository.count()).thenCallRealMethod();
Assertions.assertEquals(0, personRepository.count());
// Check that we called it 4 times
Mockito.verify(personRepository, Mockito.times(4)).count();
// Mock only with specific parameters
Person p = new Person();
Mockito.when(personRepository.findById(12L)).thenReturn(p);
Assertions.assertSame(p, personRepository.findById(12L));
Assertions.assertNull(personRepository.findById(42L));
// Mock throwing
Mockito.when(personRepository.findById(12L)).thenThrow(new WebApplicationException());
Assertions.assertThrows(WebApplicationException.class, () -> personRepository.findById(12L));
Mockito.when(personRepository.findOrdered()).thenReturn(Collections.emptyList());
Assertions.assertTrue(personRepository.findOrdered().isEmpty());
// We can even mock your custom methods
Mockito.verify(personRepository).findOrdered();
Mockito.verify(personRepository, Mockito.atLeastOnce()).findById(Mockito.any());
Mockito.verifyNoMoreInteractions(personRepository);
}
}
Hibernate ORMマッピングを単純化する方法と理由
Hibernate ORM エンティティを書くときに、ユーザーが不本意ながらも対処することに慣れてしまった、いくつかの厄介事があります:
-
IDロジックの重複:ほとんどのエンティティにはIDが必要ですが、モデルとはあまり関係がないため、ほとんどの人はIDの設定方法を気にしません。
-
オブジェクト指向アーキテクチャの通常のオブジェクトでは、ステートとメソッドが同じクラスにないことはあり得ないのに、伝統的なEEパターンでは、エンティティの定義(モデル)とそれに対する操作(DAOやリポジトリ)を分けることが推奨されており、実際にはステートとその操作を分ける必要があります。さらに、エンティティごとに2つのクラスが必要になり、エンティティの操作を行う必要があるDAOやRepositoryをインジェクションする必要があるため、編集フローが崩れ、書いているコードから抜けてインジェクションポイントを設定してから戻って使用しなければなりません。
-
Hibernateのクエリーは非常に強力ですが、一般的な操作には冗長すぎるため、すべての部分が必要ない場合でもクエリーを書く必要があります。
-
Hibernateは非常に汎用性が高いのですが、モデルの使用量の9割を占めるようなささいな操作をささいに書くことはできません。
Panacheでは、これらの問題に対して、定見に基づいたアプローチをとりました:
-
エンティティは
PanacheEntity
を拡張するようにしてください: 自動生成されるIDフィールドがあります。カスタムID戦略が必要な場合は代わりにPanacheEntityBase
を拡張するとIDを自分で処理することができます。 -
パブリックフィールドを使う。馬鹿げたゲッターとセッターを取り除きます。PanacheではないHibernate ORMでもゲッターとセッターを使う必要はありませんが、Panacheはさらに不足しているすべてのゲッターとセッターを生成し、これらのフィールドへのすべてのアクセスをアクセサメソッドを使うように書き換えます。このようにして必要なときに 有用な アクセサを書くことができ、エンティティユーザがフィールドアクセスを使用しているにもかかわらず、それが使用されます。これは、Hibernateの観点からはフィールドアクセサのように見えても、ゲッターとセッターを介してアクセサーを使用していることを意味します。
-
アクティブレコードパターンでは、すべてのエンティティロジックをエンティティクラスの静的メソッドに置き、DAOを作りません。エンティティのスーパークラスには非常に便利なスタティックメソッドがたくさん用意されていますし、エンティティクラスに独自のメソッドを追加することもできます。
Person
エンティティを使用する人はPerson.
と入力するだけで、すべての操作を一か所で完了させることができます。 -
Person.find("order by name")
やPerson.find("name = ?1 and status = ?2", "stef", Status.Alive)
、さらにはPerson.find("name", "stef")
のように、必要のない部分を書かないようにしましょう。
以上、Panacheを使えばHibernate ORMがこれほどまでにすっきりするのかということでした。
外部プロジェクトや jar でエンティティーを定義する
Hibernate ORM 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>