Simplified Hibernate Reactive with Panache

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

Hibernate Reactive 不会替代 Hibernate ORM 或 Hibernate ORM 的未来。它是一个针对反应式用例量身定制的不同堆栈,在这些用例中您需要高并发性。 此外,使用 Quarkus REST(以前称为 RESTEasy Reactive),我们的默认 REST 层不需要使用 Hibernate Reactive。将 Quarkus REST 与 Hibernate ORM 一起使用完全有效,如果你不需要高并发性,或者不习惯反应模式,建议使用 Hibernate ORM。

First: an example

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

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

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

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

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

Solution

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

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

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

Setting up and configuring Hibernate Reactive with Panache

开始:

  • application.properties 中添加您的设置

  • 使用 `@Entity`为实体添加注释

  • 使实体扩展 PanacheEntity(如果你正在使用存储库模式,则是可选的)

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

  • 带 Panache 的 Hibernate Reactive 扩展

  • 你的响应式驱动程序扩展(quarkus-reactive-pg-clientquarkus-reactive-mysql-clientquarkus-reactive-db2-client、…​)

例如:

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 中添加相关配置属性。

# 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`为其添加注释,并将你的列作为公共字段添加进去:

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

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

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

    // return name as uppercase in the model
    public String getName(){
        return name.toUpperCase();
    }

    // store all names in lowercase in the DB
    public void setName(String name){
        this.name = name.toLowerCase();
    }
}

得益于我们的字段访问重写,当你的用户读取 person.name 时,他们实际上会调用你的 getName() 访问器,同样,对于字段写入和设置器也是如此。这允许在运行时进行适当的封装,因为所有字段调用都会被相应的 getter/setter 调用替换。

Most useful operations

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

// 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 的方式。

@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 实体。

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

Defining your repository

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

@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {

   // put your custom logic here as instance methods

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

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

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

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

@Inject
PersonRepository personRepository;

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

Most useful operations

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

// 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);

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

Advanced Query

Paging

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

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

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

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

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

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

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

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

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

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

Using a range instead of pages

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

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

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

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

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

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

Sorting

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

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

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

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 优先级。

Simplified queries

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

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

  • @7 将展开为 @8

  • @9(和单个参数)将展开为 @10

  • @11 将展开为 @12

  • @13 将展开为 @14

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

  • @16 将展开为 @17

  • @18(和单个参数)将展开为 @19

  • @20 将展开为 @21

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

  • @23 将展开为 @24

  • @25(和单个参数)将展开为 @26

  • @27 将展开为 @28

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

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 查询使用命名查询。

@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 实体类别或其超类之一中定义。

Query parameters

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

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

或按名称使用 Map

Map<String, Object> params = new HashMap<>();
params.put("name", "stef");
params.put("status", Status.Alive);
Person.find("name = :name and status = :status", params);

或者使用简便类 Parameters`照原样使用或构建一个 `Map

// generate a Map
Person.find("name = :name and status = :status",
         Parameters.with("name", "stef").and("status", Status.Alive).map());

// use it as-is
Person.find("name = :name and status = :status",
         Parameters.with("name", "stef").and("status", Status.Alive));

每个查询操作接受按索引 (Object…​) 或按名称 (Map<String,Object>Parameters) 传递参数。

Query projection

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

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

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

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

import io.quarkus.runtime.annotations.RegisterForReflection;

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

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

// only 'name' will be loaded from the database
PanacheQuery<PersonName> query = Person.find("status", Status.Alive).project(PersonName.class);
1 @RegisterForReflection 注解指示 Quarkus 在本机编译期间保留类及其成员。有关 @RegisterForReflection 注解的更多详细信息,请参见 native application tips 页面。
2 这里我们使用公共字段,但如果你愿意,可以使用私有字段和 getter/setter。
3 此构造函数将由 Hibernate 使用,并且它必须有一个匹配的构造函数,其中所有类属性作为参数。

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

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

@Entity
public class Dog extends PanacheEntity {
    public String name;
    public String race;
    public Double weight;
    @ManyToOne
    public Person owner;
}

@RegisterForReflection
public class DogDto {
    public String name;
    public String ownerName;

    public DogDto(String name, @ProjectedFieldName("owner.name") String ownerName) {  (1)
        this.name = name;
        this.ownerName = ownerName;
    }
}

PanacheQuery<DogDto> query = Dog.findAll().project(DogDto.class);
1 ownerName DTO 构造函数的参数将从 owner.name HQL 属性加载。

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

@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 此注解在你要投影 @Embedded 实体或 @ManyToOne@OneToOne 关系时可以使用。它不支持 @OneToMany@ManyToMany 关系。

还可以指定具有 select 子句的 HQL 查询。在这种情况下,投影类必须具有一个与 select 子句返回的值匹配的构造函数:

import io.quarkus.runtime.annotations.RegisterForReflection;

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

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

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

// Only the race and the average weight will be loaded
PanacheQuery<RaceWeight> query = Person.find("select d.race, AVG(d.weight) from Dog d group by d.race").project(RaceWeight.class);
1 Hibernate Reactive 将使用此构造函数。当查询有选择子句时,可能有多个构造函数。

不可能同时具有 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);

Multiple Persistence Units

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

Sessions and Transactions

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

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

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

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

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

以下是一个使用刷新方法来允许在发生 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 注释。这会在事务中运行测试方法,但一旦测试方法完成,便会回滚它以恢复所有数据库更改。

Lock management

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

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

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

}

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

Custom IDs

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

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

@Entity
public class Person extends PanacheEntityBase {

    @Id
    @SequenceGenerator(
            name = "personSequence",
            sequenceName = "person_id_seq",
            allocationSize = 1,
            initialValue = 4)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "personSequence")
    public Integer id;

    //...
}

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

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

Testing

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

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

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

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 第一个响应式事务用于持久保存实体。
2 第二个响应式事务用于计数实体。
3 第三个响应式事务用于删除所有实体。

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

Mocking

Using the active record pattern

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

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

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

给定此简单实体:

@Entity
public class Person extends PanacheEntity {

    public String name;

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

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

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 确保在 Vert.x 事件循环上运行测试方法。
2 注入的 UniAsserter 参数用于进行断言。
3 请务必在 PanacheMock 上调用您的 verifydo* 方法,而不是在 Mockito 上,否则您将不知道要传递哪个模拟对象。

Using the repository pattern

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

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

给定此简单实体:

@Entity
public class Person {

    @Id
    @GeneratedValue
    public Long id;

    public String name;
}

以及此代码库:

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

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

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 确保在 Vert.x 事件循环上运行测试方法。
2 注入的 UniAsserter 参数用于进行断言。

How and why we simplify Hibernate Reactive mappings

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

  • 重复 ID 逻辑:大多数实体需要一个 ID,大多数人不在乎它是如何设置的,因为它与您的模型无关。

  • 无意义的 getter 和 setter:由于 Java 缺少对语言中属性的支持,因此我们必须创建字段,然后再为这些字段生成 getter 和 setter,即使它们实际上并未实际执行读取/写入字段之外的任何操作。

  • 传统的 EE 模式建议将实体定义(模型)与您可以在其上执行的操作(DAO、存储库)分开,但这实际上需要在状态及其操作之间进行不自然的拆分,即使我们绝不会对对象进行这样的操作面向对象架构中的常规对象,其状态和方法在同一个类中。此外,这需要每个实体两个类,并且需要注入 DAO 或存储库,您需要在其中执行实体操作,这会中断您的编辑流程并要求您退出正在重写的代码以设置注入点,然后才能回来使用它。

  • Hibernate 查询功能非常强大,但对于常见操作来说过于冗长,要求您即使不需要所有部分时也编写查询。

  • Hibernate 非常通用,但并没有让构成我们模型用法 90% 的简单操作变得简单。

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

  • 让实体继承 PanacheEntity:它具有已自动生成的 ID 字段。如果你要求自定义 ID 策略,你可以继承 `PanacheEntityBase`并自行处理 ID。

  • 使用公共字段。摆脱无意义的 getter 和 setter。在内部,我们将生成所有缺少的 getter 和 setter,并重写对这些字段的所有访问以使用访问器方法。这种方式能够在需要时仍然编写 useful 访问器,而即使实体用户仍然使用字段访问,也会使用该访问器。

  • 使用活动记录模式:将所有实体逻辑放入实体类的静态方法中,并且不要创建 DAO。实体超类附带了许多有用的静态方法,并且可以在实体类中添加自己的方法。用户只需键入 Person. 即可开始使用实体 Person,并且在一个地方获取所有操作的完成。

  • 不要编写不需要的查询部分:编写 Person.find("order by name")`或 `Person.find("name = ?1 and status = ?2", "stef", Status.Alive),甚至更好的 Person.find("name", "stef")

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

Defining entities in external projects or jars

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

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

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