Simplified Hibernate Reactive with Panache

Hibernate Reactive 是唯一的反应式 Jakarta Persistence(以前称为 JPA)实现,能够为您提供对象关系映射器的全部功能,从而允许您通过反应式驱动程序访问数据库。它使复杂的映射成为可能,但不会使简单和通用的映射变得简单。Hibernate Reactive with Panache 专注于使您的实体变得简单,并且在 Quarkus 中编写起来很有趣。

Hibernate Reactive is the only reactive Jakarta Persistence (formerly known as JPA) implementation and offers you the full breadth of an Object Relational Mapper allowing you to access your database over reactive drivers. It makes complex mappings possible, but it does not make simple and common mappings trivial. Hibernate Reactive with Panache focuses on making your entities trivial and fun to write in Quarkus.

Hibernate Reactive 不会替代 Hibernate ORM 或 Hibernate ORM 的未来。它是一个针对反应式用例量身定制的不同堆栈,在这些用例中您需要高并发性。

Hibernate Reactive is not a replacement for Hibernate ORM or the future of Hibernate ORM. It is a different stack tailored for reactive use cases where you need high-concurrency.

此外,使用 Quarkus REST(以前称为 RESTEasy Reactive),我们的默认 REST 层不需要使用 Hibernate Reactive。将 Quarkus REST 与 Hibernate ORM 一起使用完全有效,如果你不需要高并发性,或者不习惯反应模式,建议使用 Hibernate ORM。

Furthermore, using Quarkus REST (formerly RESTEasy Reactive), our default REST layer, does not require the use of Hibernate Reactive. It is perfectly valid to use Quarkus REST with Hibernate ORM, and if you do not need high-concurrency, or are not accustomed to the reactive paradigm, it is recommended to use Hibernate ORM.

First: an example

我们在 Panache 中所做的事情允许你像下面这样编写 Hibernate Reactive 实体:

What we’re doing in Panache allows you to write your Hibernate Reactive entities like this:

import io.quarkus.hibernate.reactive.panache.PanacheEntity;

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Uni<Person> findByName(String name){
        return find("name", name).firstResult();
    }

    public static Uni<List<Person>> findAlive(){
        return list("status", Status.Alive);
    }

    public static Uni<Long> deleteStefs(){
        return delete("name", "Stef");
    }
}

你注意到了代码变得多么紧凑和易读了吗?这看起来很有趣吗?继续阅读!

You have noticed how much more compact and readable the code is? Does this look interesting? Read on!

list() 方法一开始可能令人惊讶。它获取 HQL (JP-QL) 查询的片段并进行其余部分的关联。这使得代码非常简洁但仍然可读。

The list() method might be surprising at first. It takes fragments of HQL (JP-QL) queries and contextualizes the rest. That makes for very concise but yet readable code.

上面描述的本质上是 active record pattern,有时也称为实体模式。带 Panache 的 Hibernate 也允许通过 PanacheRepository 使用更经典的 repository pattern

What was described above is essentially the active record pattern, sometimes just called the entity pattern. Hibernate with Panache also allows for the use of the more classical repository pattern via PanacheRepository.

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

克隆 Git 存储库: git clone {quickstarts-clone-url},或下载 {quickstarts-archive-url}[存档]。

Clone the Git repository: git clone {quickstarts-clone-url}, or download an {quickstarts-archive-url}[archive].

该解决方案位于 hibernate-reactive-panache-quickstart directory 中。

The solution is located in the hibernate-reactive-panache-quickstart directory.

Setting up and configuring Hibernate Reactive with Panache

开始:

To get started:

  • add your settings in application.properties

  • annotate your entities with @Entity

  • make your entities extend PanacheEntity (optional if you are using the repository pattern)

在您的 pom.xml 中添加以下依赖项:

In your pom.xml, add the following dependencies:

  • the Hibernate Reactive with Panache extension

  • your reactive driver extension (quarkus-reactive-pg-client, quarkus-reactive-mysql-client, quarkus-reactive-db2-client, …​)

例如:

For instance:

pom.xml
<!-- Hibernate Reactive dependency -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-reactive-panache</artifactId>
</dependency>

<!-- Reactive SQL client for PostgreSQL -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
build.gradle
// Hibernate Reactive dependency
implementation("io.quarkus:quarkus-hibernate-reactive-panache")

Reactive SQL client for PostgreSQL
implementation("io.quarkus:quarkus-reactive-pg-client")

然后在 application.properties 中添加相关配置属性。

Then add the relevant configuration properties in application.properties.

# configure your datasource
quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = sarah
quarkus.datasource.password = connor
quarkus.datasource.reactive.url = vertx-reactive:postgresql://localhost:5432/mydatabase

# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation = drop-and-create

Solution 1: using the active record pattern

Defining your entity

要定义一个 Panache 实体,只需扩展 PanacheEntity,使用 `@Entity`为其添加注释,并将你的列作为公共字段添加进去:

To define a Panache entity, simply extend PanacheEntity, annotate it with @Entity and add your columns as public fields:

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;
}

你可以将所有 Jakarta Persistence 列注释添加到公共字段中。如果需要一个字段不被持久化,请使用 `@Transient`注释。如果你需要写入访问器,可以:

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:

@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() 访问器,同样,对于字段写入和设置器也是如此。这允许在运行时进行适当的封装,因为所有字段调用都会被相应的 getter/setter 调用替换。

And thanks to our field access rewrite, when your users read person.name they will actually call your getName() accessor, and similarly for field writes and the setter. This allows for proper encapsulation at runtime as all fields calls will be replaced by the corresponding getter/setter calls.

Most useful operations

编写实体后,以下是你能够执行的最常见操作:

Once you have written your entity, here are the most common operations you will be able to perform:

// creating a person
Person person = new Person();
person.name = "Stef";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;

// persist it
Uni<Void> persistOperation = person.persist();

// note that once persisted, you don't need to explicitly save your entity: all
// modifications are automatically persisted on transaction commit.

// check if it is persistent
if(person.isPersistent()){
    // delete it
    Uni<Void> deleteOperation = person.delete();
}

// getting a list of all Person entities
Uni<List<Person>> allPersons = Person.listAll();

// finding a specific person by ID
Uni<Person> personById = Person.findById(23L);

// finding all living persons
Uni<List<Person>> livingPersons = Person.list("status", Status.Alive);

// counting all persons
Uni<Long> countAll = Person.count();

// counting all living persons
Uni<Long> countAlive = Person.count("status", Status.Alive);

// delete all living persons
Uni<Long> deleteAliveOperation = Person.delete("status", Status.Alive);

// delete all persons
Uni<Long> deleteAllOperation = Person.deleteAll();

// delete by id
Uni<Boolean> deleteByIdOperation = Person.deleteById(23L);

// set the name of all living persons to 'Mortal'
Uni<Integer> updateOperation = Person.update("name = 'Mortal' where status = ?1", Status.Alive);

Adding entity methods

在实体本身内部向实体添加自定义查询。这样,你和你同事可以轻松地找到它们,而且查询与操作对象位于同一位置。在你的实体类中将它们作为静态方法添加是 Panache Active Record 的方式。

Add custom queries on your entities inside the entities themselves. That way, you and your co-workers can find them easily, and queries are co-located with the object they operate on. Adding them as static methods in your entity class is the Panache Active Record way.

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Uni<Person> findByName(String name){
        return find("name", name).firstResult();
    }

    public static Uni<List<Person>> findAlive(){
        return list("status", Status.Alive);
    }

    public static Uni<Long> deleteStefs(){
        return delete("name", "Stef");
    }
}

Solution 2: using the repository pattern

Defining your entity

在使用存储库模式时,您可以将您的实体定义为常规 Jakarta Persistence 实体。

When using the repository pattern, you can define your entities as regular Jakarta Persistence entities.

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

如果你不想麻烦为实体定义 getter/setter,你可以使它们扩展 PanacheEntityBase,Quarkus 会为你生成它们。你甚至可以扩展 PanacheEntity,并利用它提供的默认 ID。

If you don’t want to bother defining getters/setters for your entities, you can make them extend PanacheEntityBase and Quarkus will generate them for you. You can even extend PanacheEntity and take advantage of the default ID it provides.

Defining your repository

使用存储库时,你会得到与活动记录模式完全相同的便捷方法,注入到存储库中,通过使它们实现 PanacheRepository

When using Repositories, you get the exact same convenient methods as with the active record pattern, injected in your Repository, by making them implements PanacheRepository:

@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {

   // put your custom logic here as instance methods

   public Uni<Person> findByName(String name){
       return find("name", name).firstResult();
   }

   public Uni<List<Person>> findAlive(){
       return list("status", Status.Alive);
   }

   public Uni<Long> deleteStefs(){
       return delete("name", "Stef");
  }
}

`PanacheEntityBase`中定义的所有操作都可以在您的存储库中使用,因此使用它与使用主动纪录模式完全相同,除了您需要注入它:

All the operations that are defined on PanacheEntityBase are available on your repository, so using it is exactly the same as using the active record pattern, except you need to inject it:

@Inject
PersonRepository personRepository;

@GET
public Uni<Long> count(){
    return personRepository.count();
}

Most useful operations

在编写完存储库后,以下是您将能够执行的最常见操作:

Once you have written your repository, here are the most common operations you will be able to perform:

// creating a person
Person person = new Person();
person.setName("Stef");
person.setBirth(LocalDate.of(1910, Month.FEBRUARY, 1));
person.setStatus(Status.Alive);

// persist it
Uni<Void> persistOperation = personRepository.persist(person);

// note that once persisted, you don't need to explicitly save your entity: all
// modifications are automatically persisted on transaction commit.

// check if it is persistent
if(personRepository.isPersistent(person)){
    // delete it
    Uni<Void> deleteOperation = personRepository.delete(person);
}

// getting a list of all Person entities
Uni<List<Person>> allPersons = personRepository.listAll();

// finding a specific person by ID
Uni<Person> personById = personRepository.findById(23L);

// finding all living persons
Uni<List<Person>> livingPersons = personRepository.list("status", Status.Alive);

// counting all persons
Uni<Long> countAll = personRepository.count();

// counting all living persons
Uni<Long> countAlive = personRepository.count("status", Status.Alive);

// delete all living persons
Uni<Long> deleteLivingOperation = personRepository.delete("status", Status.Alive);

// delete all persons
Uni<Long> deleteAllOperation = personRepository.deleteAll();

// delete by id
Uni<Boolean> deleteByIdOperation = personRepository.deleteById(23L);

// set the name of all living persons to 'Mortal'
Uni<Integer> updateOperation = personRepository.update("name = 'Mortal' where status = ?1", Status.Alive);

文档的其余部分仅展示基于活动记录模式的用法,但请记住,它们也可以与存储库模式一起执行。为了简洁,存储库模式的示例已被省略。

The rest of the documentation show usages based on the active record pattern only, but keep in mind that they can be performed with the repository pattern as well. The repository pattern examples have been omitted for brevity.

Advanced Query

Paging

只有当你的表包含足够小的数据集时,才应该使用 list 方法。对于较大的数据集,可以使用其 find 方法等效项,它会返回一个 PanacheQuery,你可以对其进行分页:

You should only use the list methods if your table contains small enough data sets. For larger data sets you can use the find method equivalents, which return a PanacheQuery on which you can do paging:

// create a query for all living persons
PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);

// make it use pages of 25 entries at a time
livingPersons.page(Page.ofSize(25));

// get the first page
Uni<List<Person>> firstPage = livingPersons.list();

// get the second page
Uni<List<Person>> secondPage = livingPersons.nextPage().list();

// get page 7
Uni<List<Person>> page7 = livingPersons.page(Page.of(7, 25)).list();

// get the number of pages
Uni<Integer> numberOfPages = livingPersons.pageCount();

// get the total number of entities returned by this query without paging
Uni<Long> count = livingPersons.count();

// and you can chain methods of course
Uni<List<Person>> persons = Person.find("status", Status.Alive)
        .page(Page.ofSize(25))
        .nextPage()
        .list();

PanacheQuery 类型具有许多其他方法来处理分页和返回流。

The PanacheQuery type has many other methods to deal with paging and returning streams.

Using a range instead of pages

PanacheQuery 还允许基于范围的查询。

PanacheQuery also allows range-based queries.

// create a query for all living persons
PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);

// make it use a range: start at index 0 until index 24 (inclusive).
livingPersons.range(0, 24);

// get the range
Uni<List<Person>> firstRange = livingPersons.list();

// to get the next range, you need to call range again
Uni<List<Person>> secondRange = livingPersons.range(25, 49).list();

你无法混合范围和页面:如果你使用范围,则所有依赖于存在当前页面的方法都将抛出 UnsupportedOperationException;你可以使用 page(Page)page(int, int) 重新切换到分页。

You cannot mix ranges and pages: if you use a range, all methods that depend on having a current page will throw an UnsupportedOperationException; you can switch back to paging using page(Page) or page(int, int).

Sorting

所有接受查询字符串的方法还可以接受以下简化的查询格式:

All methods accepting a query string also accept the following simplified query form:

Uni<List<Person>> persons = Person.list("order by name,birth");

但是,这些方法还接受可选的 @1 参数,这允许您对排序进行抽象:

But these methods also accept an optional Sort parameter, which allows you to abstract your sorting:

Uni<List<Person>> persons = Person.list(Sort.by("name").and("birth"));

// and with more restrictions
Uni<List<Person>> persons = Person.list("status", Sort.by("name").and("birth"), Status.Alive);

// and list first the entries with null values in the field "birth"
Uni<List<Person>> persons = Person.list(Sort.by("birth", Sort.NullPrecedence.NULLS_FIRST));

@2 类有很多方法用于添加列和指定排序方向或 Null 优先级。

The Sort class has plenty of methods for adding columns and specifying sort direction or the null precedence.

Simplified queries

通常,HQL 查询有这种格式: @3,在末尾采用可选元素。

Normally, HQL queries are of this form: from EntityName [where …​] [order by …​], with optional elements at the end.

如果您的选择查询未以 @4、@5 或 @6 开头,我们支持以下的其他格式:

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

  • order by …​ which will expand to 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> will expand to from EntityName where <query>

如果您的更新查询未以 @15 开头,我们支持以下的其他格式:

If your update query does not start with update, we support the following additional forms:

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

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

  • set? <update-query> will expand to update EntityName set <update-query>

如果您的删除查询未以 @22 开头,我们支持以下的其他格式:

If your delete query does not start with delete, we support the following additional forms:

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

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

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

您还可以在明文中编写您的查询 @29:

You can also write your queries in plain HQL:

Order.find("select distinct o from Order o left join fetch o.lineItems");
Order.update("update from Person set name = 'Mortal' where status = ?", Status.Alive);

Named queries

您可以通过在 (简化的) HQL 查询前缀 # 字符来引用命名查询,而不是 (简化的) HQL 查询。您还可以对 count、update 和 delete 查询使用命名查询。

You can reference a named query instead of a (simplified) HQL query by prefixing its name with the '#' character. You can also use named queries for count, update and delete queries.

@Entity
@NamedQueries({
    @NamedQuery(name = "Person.getByName", query = "from Person where name = ?1"),
    @NamedQuery(name = "Person.countByStatus", query = "select count(*) from Person p where p.status = :status"),
    @NamedQuery(name = "Person.updateStatusById", query = "update Person p set p.status = :status where p.id = :id"),
    @NamedQuery(name = "Person.deleteById", query = "delete from Person p where p.id = ?1")
})
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;

    public static Uni<Person> findByName(String name){
        return find("#Person.getByName", name).firstResult();
    }

    public static Uni<Long> countByStatus(Status status) {
        return count("#Person.countByStatus", Parameters.with("status", status).map());
    }

    public static Uni<Long> updateStatusById(Status status, Long id) {
        return update("#Person.updateStatusById", Parameters.with("status", status).and("id", id));
    }

    public static Uni<Long> deleteById(Long id) {
        return delete("#Person.deleteById", id);
    }
}

命名的查询只能在 Jakarta Persistence 实体类别或其超类之一中定义。

Named queries can only be defined inside your Jakarta Persistence entity classes, or on one of their super classes.

Query parameters

你可以按索引(基于 1)传递查询参数,如下所示:

You can pass query parameters by index (1-based) as shown below:

Person.find("name = ?1 and status = ?2", "stef", Status.Alive);

或按名称使用 Map

Or by name using a 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

Or using the convenience class Parameters either as is or to build a 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) 传递参数。

Every query operation accepts passing parameters by index (Object…​), or by name (Map<String,Object> or Parameters).

Query projection

查询投影可以使用 PanacheQuery 对象上的 project(Class) 方法执行,该对象是由 find() 方法返回的。

Query projection can be done with the project(Class) method on the PanacheQuery object that is returned by the find() methods.

你可以使用它来限制数据库返回哪些字段。

You can use it to restrict which fields will be returned by the database.

Hibernate 将使用 DTO projection 并使用投影类中的属性生成一个 SELECT 子句。这也称为 dynamic instantiationconstructor expression,更多信息可以在 Hibernate 指南中找到: hql select clause

Hibernate will use DTO projection and generate a SELECT clause with the attributes from the projection class. This is also called dynamic instantiation or constructor expression, more info can be found on the Hibernate guide: hql select clause

投影类需要是一个有效的 Java Bean,并且有一个包括其所有属性的构造函数,此构造函数将用于实例化投影 DTO,而不是使用实体类。这必须是该类的唯一构造函数。

The projection class needs to be a valid Java Bean and have a constructor that contains all its attributes, this constructor will be used to instantiate the projection DTO instead of using the entity class. This must be the only constructor of the class.

import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection (1)
public class PersonName {
    public final String name; (2)

    public PersonName(String name){ (3)
        this.name = name;
    }
}

// only 'name' will be loaded from the database
PanacheQuery<PersonName> query = Person.find("status", Status.Alive).project(PersonName.class);
1 The @RegisterForReflection annotation instructs Quarkus to keep the class and its members during the native compilation. More details about the @RegisterForReflection annotation can be found on the native application tips page.
2 We use public fields here, but you can use private fields and getters/setters if you prefer.
3 This constructor will be used by Hibernate, and it must have a matching constructor with all the class attributes as parameters.

project(Class) 方法的实现使用构造函数的参数名称来构建查询的选择子句,因此必须配置编译器以存储已编译类中的参数名称。如果你正在使用 Quarkus Maven 原型,默认情况下会启用此功能。如果你没有使用它,请将属性 <maven.compiler.parameters>true</maven.compiler.parameters> 添加到你的 pom.xml。

The implementation of the project(Class) method uses the constructor’s parameter names to build the select clause of the query, so the compiler must be configured to store parameter names inside the compiled class. This is enabled by default if you are using the Quarkus Maven archetype. If you are not using it, add the property <maven.compiler.parameters>true</maven.compiler.parameters> to your pom.xml.

如果在 DTO 投影对象中,你有一个引用实体的字段,可以使用 @ProjectedFieldName 注解为 SELECT 语句提供路径。

If in the DTO projection object you have a field from a referenced entity, you can use the @ProjectedFieldName annotation to provide the path for the SELECT statement.

@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 The ownerName DTO constructor’s parameter will be loaded from the owner.name HQL property.

如果你想要以带有嵌套类的类的实体,则可以在那些嵌套类上使用 @NestedProjectedClass 注解。

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 子句返回的值匹配的构造函数:

It is also possible to specify a HQL query with a select clause. In this case, the projection class must have a constructor matching the values returned by the select clause:

import io.quarkus.runtime.annotations.RegisterForReflection;

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

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

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

// Only the race and the average weight will be loaded
PanacheQuery<RaceWeight> query = Person.find("select d.race, AVG(d.weight) from Dog d group by d.race").project(RaceWeight.class);
1 Hibernate Reactive will use this constructor. When the query has a select clause, it is possible to have multiple constructors.

不可能同时具有 HQL select new 查询和 .project(Class) - 你需要选择一种方法。

It is not possible to have a HQL select new query and .project(Class) at the same time - you need to pick one approach.

例如,这将失败:

For example, this will fail:

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

Multiple Persistence Units

Quarkus 中的 Hibernate Reactive 目前不支持多个持久化单元。

Hibernate Reactive in Quarkus currently does not support multiple persistence units.

Sessions and Transactions

首先,必须在响应式 Mutiny.Session 的范围内调用 Panache 实体的大多数方法。在某些情况下,会话将在需要时自动打开。例如,如果在包含 quarkus-rest 扩展的应用程序中,在 Jakarta REST 资源方法中调用 Panache 实体方法。对于其他情况,既有声明方式也有编程方式来确保会话已打开。你可以使用 @WithSession 注解对返回 Uni 的 CDI 业务方法进行注释。该方法将被拦截,并且返回的 Uni 将在响应式会话的范围内触发。或者,你可以使用 Panache.withSession() 方法来达到相同的效果。

First of all, most of the methods of a Panache entity must be invoked within the scope of a reactive Mutiny.Session. In some cases, the session is opened automatically on demand. For example, if a Panache entity method is invoked in a Jakarta REST resource method in an application that includes the quarkus-rest extension. For other cases, there are both a declarative and a programmatic way to ensure the session is opened. You can annotate a CDI business method that returns Uni with the @WithSession annotation. The method will be intercepted and the returned Uni will be triggered within a scope of a reactive session. Alternatively, you can use the Panache.withSession() method to achieve the same effect.

请注意,不允许从阻塞线程使用 Panache 实体。另请参阅 Getting Started With Reactive 指南,该指南解释了 Quarkus 中响应式原则的基本原理。

Note that a Panache entity may not be used from a blocking thread. See also Getting Started With Reactive guide that explains the basics of reactive principles in Quarkus.

还要确保将修改数据库或涉及多个查询的方法(例如 entity.persist())包装在事务中。你可以使用 @WithTransaction 注解对返回 Uni 的 CDI 业务方法进行注释。该方法将被拦截,并且返回的 Uni 在事务边界内触发。或者,你可以使用 Panache.withTransaction() 方法来达到相同的效果。

Also make sure to wrap methods that modify the database or involve multiple queries (e.g. entity.persist()) within a transaction. You can annotate a CDI business method that returns Uni with the @WithTransaction annotation. The method will be intercepted and the returned Uni is triggered within a transaction boundary. Alternatively, you can use the Panache.withTransaction() method for the same effect.

你不能将 @Transactional 注解与 Hibernate Reactive 一起用于你的事务:你必须使用 @WithTransaction,并且你的带注释的方法必须返回 Uni 才能是非阻塞的。

You cannot use the @Transactional annotation with Hibernate Reactive for your transactions: you must use @WithTransaction, and your annotated method must return a Uni to be non-blocking.

Hibernate Reactive 会批处理你对实体所做的更改,并在事务结束时或在查询前发送更改(称为刷新)。这通常是一件好事,因为它更高效。但是,如果你想检查乐观锁定失败、立即进行对象验证或通常想获得立即反馈,你可以通过调用 entity.flush() 来强制刷新操作,甚至使用 entity.persistAndFlush() 使其成为一个单一方法调用。这将允许你捕获当 Hibernate Reactive 将这些更改发送到数据库时可能发生的任何 PersistenceException。请记住,这样做效率较低,因此不要滥用它。而且你的事务仍必须提交。

Hibernate Reactive 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 Reactive send those changes to the database. Remember, this is less efficient so don’t abuse it. And your transaction still has to be committed.

以下是一个使用刷新方法来允许在发生 PersistenceException 的情况下执行特定操作的示例:

Here is an example of the usage of the flush method to allow making a specific action in case of PersistenceException:

@WithTransaction
public Uni<Void> create(Person person){
    // Here we use the persistAndFlush() shorthand method on a Panache repository to persist to database then flush the changes.
    return person.persistAndFlush()
            .onFailure(PersistenceException.class)
            .recoverWithItem(() -> {
                LOG.error("Unable to create the parameter", pe);
                //in case of error, I save it to disk
                diskPersister.save(person);
                return null;
            });
}

@WithTransaction 注释也将适用于测试。这意味着测试期间所做的更改将传播到数据库。如果您希望在测试结束时恢复所做的任何更改,可以使用 io.quarkus.test.TestReactiveTransaction 注释。这会在事务中运行测试方法,但一旦测试方法完成,便会回滚它以恢复所有数据库更改。

The @WithTransaction annotation will also work for testing. This means that changes done during the test will be propagated to the database. If you want any changes made to be rolled back at the end of the test you can use the io.quarkus.test.TestReactiveTransaction annotation. This will run the test method in a transaction, but roll it back once the test method is complete to revert any database changes.

Lock management

Panache 提供对数据库锁定使用您的实体/存储库的直接支持,方法是使用 findById(Object, LockModeType)find().withLock(LockModeType)

Panache provides direct support for database locking with your entity/repository, using findById(Object, LockModeType) or find().withLock(LockModeType).

以下示例适用于主动记录模式,但同样适用于存储库。

The following examples are for the active record pattern, but the same can be used with repositories.

First: Locking using findById().

public class PersonEndpoint {

    @GET
    public Uni<Person> findByIdForUpdate(Long id){
        return Panache.withTransaction(() -> {
            return Person.<Person>findById(id, LockModeType.PESSIMISTIC_WRITE)
                    .invoke(person -> {
                        //do something useful, the lock will be released when the transaction ends.
                    });
        });
    }
}

Second: Locking in a find().

public class PersonEndpoint {

    @GET
    public Uni<Person> findByNameForUpdate(String name){
        return Panache.withTransaction(() -> {
            return Person.<Person>find("name", name).withLock(LockModeType.PESSIMISTIC_WRITE).firstResult()
                    .invoke(person -> {
                        //do something useful, the lock will be released when the transaction ends.
                    });
        });
    }

}

注意,当事务结束时锁将被释放,因此调用锁定查询的方法必须在事务中调用。

Be careful that locks are released when the transaction ends, so the method that invokes the lock query must be called within a transaction.

Custom IDs

ID 常常是一个敏感的主题,并不是每个人都愿意让框架处理它们,又一次我们为您提供支持。

IDs are often a touchy subject, and not everyone’s up for letting them handled by the framework, once again we have you covered.

您可以通过扩展 PanacheEntityBase 而不是 PanacheEntity 来指定自己的 ID 策略。然后您只需将您想要的任何 ID 声明为一个公共字段:

You can specify your own ID strategy by extending PanacheEntityBase instead of PanacheEntity. Then you just declare whatever ID you want as a public field:

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

    //...
}

如果您正在使用存储库,那么您需要扩展 PanacheRepositoryBase 而不是 PanacheRepository ,并将您的 ID 类型指定为一个额外的类型参数:

If you’re using repositories, then you will want to extend PanacheRepositoryBase instead of PanacheRepository and specify your ID type as an extra type parameter:

@ApplicationScoped
public class PersonRepository implements PanacheRepositoryBase<Person,Integer> {
    //...
}

Testing

@QuarkusTest 中测试 reactive Panache 实体比测试常规 Panache 实体要复杂一些,这是因为 API 是异步的,并且所有操作都需要在 Vert.x 事件循环上运行。

Testing reactive Panache entities in a @QuarkusTest is slightly more complicated than testing regular Panache entities due to the asynchronous nature of the APIs and the fact that all operations need to run on a Vert.x event loop.

quarkus-test-vertx 依赖项提供 @io.quarkus.test.vertx.RunOnVertxContext 注释和 io.quarkus.test.vertx.UniAsserter 类,这些注释和类正是用于此目的。使用说明在 Hibernate Reactive 指南中进行了描述。

The quarkus-test-vertx dependency provides the @io.quarkus.test.vertx.RunOnVertxContext annotation and the io.quarkus.test.vertx.UniAsserter class which are intended precisely for this purpose. The usage is described in the Hibernate Reactive guide.

此外,quarkus-test-hibernate-reactive-panache 依赖关系可提供 io.quarkus.test.hibernate.reactive.panache.TransactionalUniAsserter,其可注入到使用 @RunOnVertxContext 注释标注的测试方法的方法参数中。TransactionalUniAsserter 是一个 io.quarkus.test.vertx.UniAsserterInterceptor,它在单独的反应事务中包装每个断言方法。

Moreover, the quarkus-test-hibernate-reactive-panache dependency provides the io.quarkus.test.hibernate.reactive.panache.TransactionalUniAsserter that can be injected as a method parameter of a test method annotated with @RunOnVertxContext. The TransactionalUniAsserter is a io.quarkus.test.vertx.UniAsserterInterceptor that wraps each assert method within a separate reactive transaction.

TransactionalUniAsserter Example
import io.quarkus.test.hibernate.reactive.panache.TransactionalUniAsserter;

@QuarkusTest
public class SomeTest {

    @Test
    @RunOnVertxContext
    public void testEntity(TransactionalUniAsserter asserter) {
        asserter.execute(() -> new MyEntity().persist()); 1
        asserter.assertEquals(() -> MyEntity.count(), 1l); 2
        asserter.execute(() -> MyEntity.deleteAll()); 3
    }
}
1 The first reactive transaction is used to persist the entity.
2 The second reactive transaction is used to count the entities.
3 The third reactive transaction is used to delete all entities.

当然,您还可定义一个自定义 UniAsserterInterceptor,以包装注入的 UniAsserter 并自定义行为。

Of course, you can also define a custom UniAsserterInterceptor to wrap the injected UniAsserter and customize the behavior.

Mocking

Using the active record pattern

如果您正在使用主动记录模式,则您无法直接使用 Mockito,因为它不支持模拟静态方法,但您可以使用 quarkus-panache-mock 模块,它允许您使用 Mockito 模拟所有提供的静态方法,包括您自己的。

If you are using the active record pattern you cannot use Mockito directly as it does not support mocking static methods, but you can use the quarkus-panache-mock module which allows you to use Mockito to mock all provided static methods, including your own.

将此依赖项添加到您的构建文件中:

Add this dependency to your build file:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-panache-mock</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-panache-mock")

给定此简单实体:

Given this simple entity:

@Entity
public class Person extends PanacheEntity {

    public String name;

    public static Uni<List<Person>> findOrdered() {
        return find("ORDER BY name").list();
    }
}

可以像这样编写模拟测试:

You can write your mocking test like this:

import io.quarkus.test.vertx.UniAsserter;
import io.quarkus.test.vertx.RunOnVertxContext;

@QuarkusTest
public class PanacheFunctionalityTest {

    @RunOnVertxContext (1)
    @Test
    public void testPanacheMocking(UniAsserter asserter) { (2)
        asserter.execute(() -> PanacheMock.mock(Person.class));

        // Mocked classes always return a default value
        asserter.assertEquals(() -> Person.count(), 0l);

        // Now let's specify the return value
        asserter.execute(() -> Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(23l)));
        asserter.assertEquals(() -> Person.count(), 23l);

        // Now let's change the return value
        asserter.execute(() -> Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(42l)));
        asserter.assertEquals(() -> Person.count(), 42l);

        // Now let's call the original method
        asserter.execute(() -> Mockito.when(Person.count()).thenCallRealMethod());
        asserter.assertEquals(() -> Person.count(), 0l);

        // Check that we called it 4 times
        asserter.execute(() -> {
            PanacheMock.verify(Person.class, Mockito.times(4)).count(); (3)
        });

        // Mock only with specific parameters
        asserter.execute(() -> {
            Person p = new Person();
            Mockito.when(Person.findById(12l)).thenReturn(Uni.createFrom().item(p));
            asserter.putData(key, p);
        });
        asserter.assertThat(() -> Person.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key)));
        asserter.assertNull(() -> Person.findById(42l));

        // Mock throwing
        asserter.execute(() -> Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException()));
        asserter.assertFailedWith(() -> {
            try {
                return Person.findById(12l);
            } catch (Exception e) {
                return Uni.createFrom().failure(e);
            }
        }, t -> assertEquals(WebApplicationException.class, t.getClass()));

        // We can even mock your custom methods
        asserter.execute(() -> Mockito.when(Person.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList())));
        asserter.assertThat(() -> Person.findOrdered(), list -> list.isEmpty());

        asserter.execute(() -> {
            PanacheMock.verify(Person.class).findOrdered();
            PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any());
            PanacheMock.verifyNoMoreInteractions(Person.class);
        });

        // IMPORTANT: We need to execute the asserter within a reactive session
        asserter.surroundWith(u -> Panache.withSession(() -> u));
    }
}
1 Make sure the test method is run on the Vert.x event loop.
2 The injected UniAsserter argument is used to make assertions.
3 Be sure to call your verify and do* methods on PanacheMock rather than Mockito, otherwise you won’t know what mock object to pass.

Using the repository pattern

如果正在使用仓库模式,则可以使用 @5 模块直接使用 Mockito,这会使模拟 Bean 变得更容易:

If you are using the repository pattern you can use Mockito directly, using the quarkus-junit5-mockito module, which makes mocking beans much easier:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-junit5-mockito")

给定此简单实体:

Given this simple entity:

@Entity
public class Person {

    @Id
    @GeneratedValue
    public Long id;

    public String name;
}

以及此代码库:

And this repository:

@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {
    public Uni<List<Person>> findOrdered() {
        return find("ORDER BY name").list();
    }
}

可以像这样编写模拟测试:

You can write your mocking test like this:

import io.quarkus.test.vertx.UniAsserter;
import io.quarkus.test.vertx.RunOnVertxContext;

@QuarkusTest
public class PanacheFunctionalityTest {
    @InjectMock
    PersonRepository personRepository;

    @RunOnVertxContext (1)
    @Test
    public void testPanacheRepositoryMocking(UniAsserter asserter) { (2)

        // Mocked classes always return a default value
        asserter.assertEquals(() -> mockablePersonRepository.count(), 0l);

        // Now let's specify the return value
        asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(23l)));
        asserter.assertEquals(() -> mockablePersonRepository.count(), 23l);

        // Now let's change the return value
        asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(42l)));
        asserter.assertEquals(() -> mockablePersonRepository.count(), 42l);

        // Now let's call the original method
        asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenCallRealMethod());
        asserter.assertEquals(() -> mockablePersonRepository.count(), 0l);

        // Check that we called it 4 times
        asserter.execute(() -> {
            Mockito.verify(mockablePersonRepository, Mockito.times(4)).count();
        });

        // Mock only with specific parameters
        asserter.execute(() -> {
            Person p = new Person();
            Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(Uni.createFrom().item(p));
            asserter.putData(key, p);
        });
        asserter.assertThat(() -> mockablePersonRepository.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key)));
        asserter.assertNull(() -> mockablePersonRepository.findById(42l));

        // Mock throwing
        asserter.execute(() -> Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException()));
        asserter.assertFailedWith(() -> {
            try {
                return mockablePersonRepository.findById(12l);
            } catch (Exception e) {
                return Uni.createFrom().failure(e);
            }
        }, t -> assertEquals(WebApplicationException.class, t.getClass()));

        // We can even mock your custom methods
        asserter.execute(() -> Mockito.when(mockablePersonRepository.findOrdered())
                .thenReturn(Uni.createFrom().item(Collections.emptyList())));
        asserter.assertThat(() -> mockablePersonRepository.findOrdered(), list -> list.isEmpty());

        asserter.execute(() -> {
            Mockito.verify(mockablePersonRepository).findOrdered();
            Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any());
            Mockito.verify(mockablePersonRepository).persist(Mockito.<Person> any());
            Mockito.verifyNoMoreInteractions(mockablePersonRepository);
        });

        // IMPORTANT: We need to execute the asserter within a reactive session
        asserter.surroundWith(u -> Panache.withSession(() -> u));
    }
}
1 Make sure the test method is run on the Vert.x event loop.
2 The injected UniAsserter agrument is used to make assertions.

How and why we simplify Hibernate Reactive mappings

在编写 Hibernate 响应式实体时,有一些令人烦恼的事情让用户不得不勉强习惯处理,例如:

When it comes to writing Hibernate Reactive entities, there are a number of annoying things that users have grown used to reluctantly deal with, such as:

  • Duplicating ID logic: most entities need an ID, most people don’t care how it is set, because it is not really relevant to your model.

  • Dumb getters and setters: since Java lacks support for properties in the language, we have to create fields, then generate getters and setters for those fields, even if they don’t actually do anything more than read/write the fields.

  • Traditional EE patterns advise to split entity definition (the model) from the operations you can do on them (DAOs, Repositories), but really that requires an unnatural split between the state and its operations even though we would never do something like that for regular objects in the Object-Oriented architecture, where state and methods are in the same class. Moreover, this requires two classes per entity, and requires injection of the DAO or Repository where you need to do entity operations, which breaks your edit flow and requires you to get out of the code you’re writing to set up an injection point before coming back to use it.

  • Hibernate queries are super powerful, but overly verbose for common operations, requiring you to write queries even when you don’t need all the parts.

  • Hibernate is very general-purpose, but does not make it trivial to do trivial operations that make up 90% of our model usage.

通过 Panache,我们采取了一个明确的方法来解决所有这些问题:

With Panache, we took an opinionated approach to tackle all these problems:

  • Make your entities extend PanacheEntity: it has an ID field that is auto-generated. If you require a custom ID strategy, you can extend PanacheEntityBase instead and handle the ID yourself.

  • Use public fields. Get rid of dumb getter and setters. Under the hood, we will generate all getters and setters that are missing, and rewrite every access to these fields to use the accessor methods. This way you can still write useful accessors when you need them, which will be used even though your entity users still use field accesses.

  • With the active record pattern: put all your entity logic in static methods in your entity class and don’t create DAOs. Your entity superclass comes with lots of super useful static methods, and you can add your own in your entity class. Users can just start using your entity Person by typing Person. and getting completion for all the operations in a single place.

  • Don’t write parts of the query that you don’t need: write Person.find("order by name") or Person.find("name = ?1 and status = ?2", "stef", Status.Alive) or even better Person.find("name", "stef").

这就是它的全部内容:有了 Panache,Hibernate Reactive 看起来从未如此精简和简洁。

That’s all there is to it: with Panache, Hibernate Reactive has never looked so trim and neat.

Defining entities in external projects or jars

Hibernate Reactive with Panache 依赖于编译时字节码对您的实体进行增强。如果您在构建 Quarkus 应用程序的同一项目中定义实体,那么一切都会正常工作。

Hibernate Reactive with Panache 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.

如果实体来自外部项目或 jar,你可以通过添加一个空的 META-INF/beans.xml 文件来确保你的 jar 像 Quarkus 应用程序库一样被处理。

If the entities come from external projects or jars, you can make sure that your jar is treated like a Quarkus application library by adding an empty META-INF/beans.xml file.

这将允许 Quarkus 索引和增强你的实体,就好像它们在当前项目中一样。

This will allow Quarkus to index and enhance your entities as if they were inside the current project.