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アノテーションプロセッサーを追加する必要があります:

pom.xml
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${compiler-plugin.version}</version>
    <configuration>
        <parameters>${maven.compiler.parameters}</parameters>
        <annotationProcessorPaths>
            <!-- Your existing annotation processor(s)... -->
            <path>
                <groupId>io.quarkus</groupId>
                <artifactId>quarkus-panache-common</artifactId>
                <version>${quarkus.platform.version}</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>
build.gradle
annotationProcessor("io.quarkus:quarkus-panache-common")

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;
}

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 を追加してください。
pom.xml
<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();

レンジとページを混在させることはできません。レンジを使用した場合、現在のページを持っていることに依存するすべてのメソッドは 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 …​] というように最後にオプションの要素を持つという形式になっています。

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

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

  • <singleColumnName> (およびシングルパラメータ は from EntityName where <singleColumnName> = ? に展開されます

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

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

  • from EntityName …​ which will expand to update EntityName …​

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

  • set? <update-query> will expand to update 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 によって使用されます。このコンストラクタはクラス内の唯一のコンストラクタであり、パラメータとしてクラスのすべての属性を持つ必要があります。

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

また、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 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 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 を直接使用することができます。これにより、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>