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

シンプルになった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によるHibernate ORMのセットアップと設定

始めるには:

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

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

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

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

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

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

  • お使いの JDBC ドライバーエクステンション ( quarkus-jdbc-postgresql , quarkus-jdbc-h2 , quarkus-jdbc-mariadb , …​)

pom.xml
<!-- 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>
build.gradle
// 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;
}

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

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リソースの書き方

First, include one of the Quarkus REST (formerly RESTEasy Reactive) extensions to enable Jakarta REST endpoints, for example, add the io.quarkus:quarkus-rest-jackson dependency for Jakarta REST and JSON support.

そして、次のようなリソースを作成することで、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 アノテーションを使用するように注意してください。わかりやすくするために、クラスレベルでアノテーションを追加することもできます。

DevモードのQuarkusでHibernate ORM with Panacheの機能を簡単に紹介するために、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;
本番環境でQuarkusアプリを起動したときにDBを初期化したい場合は、 import.sql に加えて、Quarkusの起動オプションに quarkus.hibernate-orm.database.generation=drop-and-create を追加します。

その後、人物リストを表示し、新しい人物を次のように追加することができます:

$ 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"}]
If you see the Person object as Person<1>, then the object has not been converted. In this case, add the dependency quarkus-rest-jackson in pom.xml.
pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-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();

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

ソート

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

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 …​] というように最後にオプションの要素を持つという形式になっています。

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

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

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

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

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

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

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

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

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

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

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

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

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

また、プレーンな HQL でクエリを記述することもできます:
Order.find("select distinct o from Order o left join fetch o.lineItems");
Order.update("update 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);
    }
}

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

クエリーパラメーター

以下のように、インデックス(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プロジェクション を使用し、プロジェクションクラスからの属性を持つ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 によって使用されます。このコンストラクタはクラス内の唯一のコンストラクタであり、パラメータとしてクラスのすべての属性を持つ必要があります。

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

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プロパティから読み込まれます。

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

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

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

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

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

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

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

import io.quarkus.runtime.annotations.RegisterForReflection;

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

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

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

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

HQL の`select new` クエリと .project(Class) を同時に使えません。どちらかの方法を選択してください。

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

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

複数の永続化ユニット

複数の永続化ユニットのサポートについては Hibernate ORMガイドで詳しく説明されています。

Panacheの使い方は簡単です:

  • 1つのPanacheエンティティは、1つの永続化ユニットにしかアタッチできません。

  • そう考えると、Panacheはすでに、Panacheエンティティに関連する適切な EntityManager を透過的に見つけるために必要な導線を提供しています。

トランザクション

データベースを変更するメソッド (例: entity.persist() ) をトランザクション内でラップするようにしてください。CDI Beanメソッド @Transactional をマークすることで、それを実現出来、そのメソッドをトランザクションの境界に出来ます。REST エンドポイントコントローラーのように、アプリケーションのエントリーポイントの境界でこれを行うことをお勧めします。

Hibernate ORMは、エンティティに加えた変更をバッチして、トランザクションの最後やクエリの前に変更を送信します(これをフラッシュと呼びます)。これは通常、より効率的であるため良いことです。しかし、楽観的なロックの失敗をチェックしたり、オブジェクトの検証をすぐに行ったり、一般的にすぐにフィードバックを得たい場合は、 entity.flush() 、あるいは entity.persistAndFlush() を使用して単一のメソッド呼び出しにすることで、フラッシュ操作を強制することができます。これにより、Hibernate ORMがデータベースに変更を送信する際に発生する可能性のある PersistenceException をキャッチすることができます。しかし、これは効率が悪いので、乱用しないようにしましょう。そして、トランザクションは依然としてコミットされる必要があります。

ここでは 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 verifydo* のメソッドは Mockito ではなく PanacheMock で呼び出すようにしてください。そうしないとどのモックオブジェクトを渡せばいいのかわからなくなってしまいます。

EntityManagerSession とエンティティインスタンスのメソッドのモック化

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 を直接使用することができます。これにより、ビーンのモッキングが非常に簡単になります。

<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 in Quarkus relies on compile-time bytecode enhancements to your entities. If you define your entities in the same project where you build your Quarkus application, everything will work fine.

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

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

関連コンテンツ