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

使用Panache简化Hibernate Reactive

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 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.

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.

首先看一个例子

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

你有注意到代码的紧凑性和可读性大大提高了吗?看起来很有趣吧?请继续阅读!

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.
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.

解决方案

我们建议您按照下一节的说明逐步创建应用程序。然而,您可以直接转到已完成的示例。

克隆 Git 仓库可使用命令: git clone https://github.com/quarkusio/quarkus-quickstarts.git ,或者下载 压缩包

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

在Hibernate Reactive中配置Panache

按以下步骤开始:

  • application.properties 中添加你的设置。

  • 给实体类增加 @Entity 注解

  • 实体类改为继承 PanacheEntity 类(使用Repository模式时为可选操作)。

按照 Hibernate 设置指南 进行其他配置。

在你的 pom.xml 文件中添加以下依赖项:

  • Hibernate Reactive with Panache扩展

  • 响应式驱动扩展 ( quarkus-reactive-pg-client , quarkus-reactive-mysql-client , quarkus-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

解决方案1:使用Active Record模式

定义实体类

要定义一个Panache实体类,只需继承 PanacheEntity ,增加 @Entity 注解,并将数据库列作为公共字段添加到实体类:

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

@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调用所取代。

常用操作

写好了实体类之后,你可以执行以下一些最常见的操作:

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

添加实体方法

在实体本身内部的实体上添加自定义查询。这样,您和您的同事可以轻松找到它们,并且查询与他们操作的对象位于同一位置。将它们作为静态方法添加到实体类中是 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");
    }
}

解决方案2:使用Repository模式

定义实体类

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将自动生成getter和setter方法。你也可以继承 PanacheEntity ,相比 PanacheEntityBase ,其优势是它还提供默认的ID字段。

定义Repository

使用 Repository 模式时,通过实现 PanacheRepository 接口,你可以使用与Active Record模式下完全相同的便捷方法:

@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 中定义的所有方法都可以在你的Repository类上使用,所以它使用起来与Active Record模式完全一样,只是你需要注入Repository类的实例:

@Inject
PersonRepository personRepository;

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

常用操作

写好了Repository类之后,你可以执行以下一些最常见的操作:

// 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);
其余的文档只展示了基于活动记录模式的用法,但请记住,这些用法也可以用资源库模式来执行。为了简洁起见,已省略存储库模式示例。

高级查询

分页

如果你的表数据量很小,你应该只用到 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 类还有很多其他方法来做分页查询、返回流。

使用范围查询而替代分页查询

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

你不能混合使用ranges和pages:如果你使用range,所有依赖于拥有当前页面的方法将抛出一个 UnsupportedOperationException ;你可以使用 page(Page)page(int, int) 切换回分页。

排序

所有接收查询字符串的方法也能接收以下简化形式的查询:

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

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

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

简化查询

通常情况下,HQL查询语句是这种形式: from EntityName [where …​​] [order by …​​] ,结尾处有可选元素。

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

  • order by …​ 语句会被扩展为: from EntityName order by …​

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

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

  • <query> 语句会被扩展为: from EntityName where <query>

如果你的更新语句不是以 update 开始,我们还支持以下的形式:

  • from EntityName …​ 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>

如果你的删除语句不是以 delete 开始,我们还支持以下的形式:

  • from EntityName …​​ 语句会被扩展为: delete from EntityName …​​

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

  • <query> 语句会被扩展为: delete from EntityName where <query>

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

命名查询

除了上述的简化HQL查询以外,你还可以定义一个命名查询,然后通过'#'字符加命名来(在HQL中)引用它。在计数、更新和删除查询中也可以使用命名查询。

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

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 ,用key做参数名:

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 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,而不是使用实体类。这个构造方法必须是唯一的构造方法。

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在native编译过程中保留该类和其成员。关于 @RegisterForReflection 注解的更多细节可以在 native应用程序提示 页面找到。
2 在这里我们使用public的字段,你也可以使用private字段和对应的getter/setter方法。
3 Hibernate会用到这个构造方法,它必须有一个匹配的构造函数,所有的类属性都是参数。

project(Class) 方法的实现中,使用构造函数的参数名来构建查询的select子句,所以编译器必须配置为在编译的类中保留参数名。如果是使用Quarkus Maven archetype创建的项目,该功能默认是启用的。如果你没有使用,请在你的 pom.xml 中添加该属性 <maven.compiler.parameters>true</maven.compiler.parameters>

如果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 DTO构造器的 ownerName 参数将从 owner.name HQL属性加载。

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

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

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

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

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

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

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.

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

多个持久化单元

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

Sessions and Transactions

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.

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.

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.

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 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.

下面是一个使用 flush 方法的例子,它在捕获到 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;
            });
}

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.

锁管理

Panache支持在实体类/Repository类中直接使用数据库的锁,可使用 findById(Object, LockModeType)find().withLock(LockModeType) 方法。

下面的例子是针对Active Record模式的,但同样可以应用于Repository模式。

第一:通过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.
                    });
        });
    }
}

第二:通过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.
                    });
        });
    }

}

请注意,事务结束时锁会被释放,所以带锁查询的方法必须在事务中调用。

自定义ID

ID往往是一个敏感的话题,并不是所有人都愿意让框架来处理,因此我们提供了相应的配置。

你可以通过继承 PanacheEntityBase ,而非 PanacheEntity ,来指定你自己的ID策略。然后只要把你想要的ID字段声明为public字段:

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

    //...
}

如果你使用Repository模式,那么要继承 PanacheRepositoryBase ,而非 PanacheRepository ,并将ID字段类型作为额外的类型参数:

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

测试

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.

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.

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.

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

Mock模拟测试

使用Active Record模式

如果你使用了Active Record模式,那么不能直接使用Mockito,因为它不支持Mock静态方法。你可以使用 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 Make sure the test method is run on the Vert.x event loop.
2 The injected UniAsserter argument is used to make assertions.
3 请确保是在 PanacheMock 上调用 verifydo* 方法,而不是在 Mockito 上调用,否则无法传递mock对象。

使用Repository模式

如果你使用存储库模式,你可以直接使用Mockito,使用 quarkus-junit5-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;
}

以及这个Repository类:

@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 Make sure the test method is run on the Vert.x event loop.
2 The injected UniAsserter agrument is used to make assertions.

我们为什么简化Hibernate Reactive映射?怎么做到的?

在编写Hibernate Reactive实体类时,用户已经习惯了被迫处理许多烦人的事情,例如:

  • 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.

  • 繁琐的 getters 和 setters:由于Java语言中缺乏对属性的支持,我们必须创建字段,然后为这些字段getters 和 setters,即使它们除了read/write字段外实际上没有做任何事情。

  • 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查询功能很强大,但对于普通操作来说过于冗长,即使是简单操作也要求写完整的HQL语句。

  • Hibernate很通用,但对于模型里90%的琐碎操作,编写起来并不简单。

我们通过Panache采取了一种有主见的方案来解决以上这些问题:

  • 让你的实体类继承 PanacheEntity :它有一个自动生成的ID字段。如果你需要自定义ID策略,可以继承 PanacheEntityBase ,而不用自己处理ID。

  • 使用公共字段。摆脱繁琐的getter和setters。在后台,我们将生成所有缺失的getter和setter,并重写对这些字段的每个访问,以使用访问器方法。这样,当你需要时,你仍然可以写出 有用的 访问器,即使你的实体用户仍然使用字段访问,也会被使用。

  • 使用活动记录模式:把你所有的实体逻辑放在实体类的静态方法中,不要创建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看起来变得如此轻量和整洁。

在外部项目或jar包中定义实体

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.

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.

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

Related content