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

首先看一个例子

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

@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模式 ,有时也称为实体模式。Panache也支持通过 PanacheRepository 使用更经典的 Repository模式

解决方案

我们建议你按照下面几节的说明,一步一步地创建应用程序。当然,你也可以直接使用已完成的样例工程。

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

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

If your project is already configured to use other annotation processors, you will need to additionally add the Panache annotation processor:

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

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

这些公共字段可以添加任何JPA列注解。如果你不想持久化某个字段,给它增加 @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 方法调用。

常用操作

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

// 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's 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模式

定义实体类

使用Repository模式时,可以将实体类定义为普通的JPA实体。

@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's 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);
后续的文档只展示了基于Active Record模式的用法,但请记住,这些用法也可以用于Repository模式。为了简洁起见,省略了Repository模式的例子。

高级查询

分页

如果你的表数据量很小,你应该只用到 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();

但不能混用范围查询和分页查询。当你使用范围查询时,调用任意依赖于分页查询的方法都会抛出一个 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 …​​] ,结尾处有可选元素。

如果你的查询语句不是以 from 开始,我们还支持以下的形式:

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

  • <singleColumnName> (带单个参数)语句会被扩展为: from EntityName where <singleColumnName> = ?

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

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

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

  • set? <singleColumnName> (带单个参数)语句会被扩展为: update from EntityName set <singleColumnName> = ?

  • set? <update-query> 语句会被扩展为: update from EntityName set <update-query>

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

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

  • <singleColumnName> (带单个参数)语句会被扩展为: delete from EntityName where <singleColumnName> = ?

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

你也可以用普通的 HQL 编写查询语句:
Order.find("select distinct o from Order o left join fetch o.lineItems");
Order.update("update 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);
    }
}

命名查询只能在JPA实体类(必须是Panache实体类,或Repository类的参数化类型)内定义,或在它的父类中定义。

查询参数

你可以通过索引(从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会使用 DTO投影 ,并根据投影的类的属性生成SELECT子句。这也被称为 动态实例化构造器表达 ,更多信息可以在Hibernate指南中找到: HQL select子句

投影类必须是有效的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;
    @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属性加载。

多个持久化单元

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

事务

请确保修改数据库的方法(例如: entity.persist() )处于同一个事务中。给一个CDI bean方法增加 @ReactiveTransactional 注解,可以确保该方法即事务边界。另外,你也可以使用 Panache.withTransaction() 来达到同样的效果。我们建议在应用端点的边界这样做,比如REST端点的Controller。

You cannot use @Transactional with Hibernate Reactive for your transactions: you must use @ReactiveTransactional, and your annotated method must return a Uni to be non-blocking. Otherwise, it needs be called from a non-VertxThread thread and will become blocking.

JPA将实体的变更进行批量处理,并在事务结束时或查询前发送批量变更(这被称为flush)。这通常更有效率。但是当你想检查乐观锁的失败,即时进行对象验证,或者想得到即时的反馈,你可以通过调用 entity.flush()entity.persistAndFlush() 强制执行flush。JPA向数据库发送这些变更时可能会抛出 PersistenceException ,你可以捕捉这些异常。记住,这样做的效率较低,所以不要滥用它。而且你的事务仍然需要提交。

下面是一个使用 flush 方法的例子,它在捕获到 PersistenceException 异常时执行指定操作:

@ReactiveTransactional
public Uni<Void> create(Person person){
    //Here I 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;
            });
}

@ReactiveTransactional 注解也可用于测试。这意味着在测试期间所做数据变更将被持久化到数据库中。如果你想在测试结束时回滚所有数据库变更,可以使用 io.quarkus.test.TestReactiveTransaction 注解。加入这个注解后将在一个事务中运行测试方法,当测试方法完成,就会回滚事务,以恢复所有数据库的变更。

锁管理

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> {
    //...
}

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

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

@QuarkusTest
public class PanacheFunctionalityTest {

    @Test
    public void testPanacheMocking() {
        PanacheMock.mock(Person.class);

        // Mocked classes always return a default value
        Assertions.assertEquals(0, Person.count().await().indefinitely());

        // Now let's specify the return value
        Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(23l));
        Assertions.assertEquals(23, Person.count().await().indefinitely());

        // Now let's change the return value
        Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(42l));
        Assertions.assertEquals(42, Person.count().await().indefinitely());

        // Now let's call the original method
        Mockito.when(Person.count()).thenCallRealMethod();
        Assertions.assertEquals(0, Person.count().await().indefinitely());

        // Check that we called it 4 times
        PanacheMock.verify(Person.class, Mockito.times(4)).count();(1)

        // Mock only with specific parameters
        Person p = new Person();
        Mockito.when(Person.findById(12l)).thenReturn(Uni.createFrom().item(p));
        Assertions.assertSame(p, Person.findById(12l).await().indefinitely());
        Assertions.assertNull(Person.findById(42l).await().indefinitely());

        // Mock throwing
        Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException());
        try {
            Person.findById(12l);
            Assertions.fail();
        } catch (WebApplicationException x) {
        }

        // We can even mock your custom methods
        Mockito.when(Person.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList()));
        Assertions.assertTrue(Person.findOrdered().await().indefinitely().isEmpty());

        PanacheMock.verify(Person.class).findOrdered();
        PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any());
        PanacheMock.verifyNoMoreInteractions(Person.class);
    }
}
1 请确保是在 PanacheMock 上调用 verifydo* 方法,而不是在 Mockito 上调用,否则无法传递mock对象。

模拟 Mutiny.Session , 及实体类的实例方法

如果你需要模拟实体类的实例方法,比如 persist() ,可以通过模拟Hibernate Reactive的 Mutiny.Session 对象来实现:

@QuarkusTest
public class PanacheMockingTest {

    @InjectMock
    Mutiny.Session session;

    @Test
    public void testPanacheSessionMocking() {
        Person p = new Person();
        // mocked via Mutiny.Session mocking
        p.persist().await().indefinitely();
        Assertions.assertNull(p.id);

        Mockito.verify(session, Mockito.times(1)).persist(Mockito.any());
    }
}

使用Repository模式

如果你使用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();
    }
}

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

@QuarkusTest
public class PanacheFunctionalityTest {
    @InjectMock
    PersonRepository personRepository;

    @Test
    public void testPanacheRepositoryMocking() throws Throwable {
        // Mocked classes always return a default value
        Assertions.assertEquals(0, mockablePersonRepository.count().await().indefinitely());

        // Now let's specify the return value
        Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(23l));
        Assertions.assertEquals(23, mockablePersonRepository.count().await().indefinitely());

        // Now let's change the return value
        Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(42l));
        Assertions.assertEquals(42, mockablePersonRepository.count().await().indefinitely());

        // Now let's call the original method
        Mockito.when(mockablePersonRepository.count()).thenCallRealMethod();
        Assertions.assertEquals(0, mockablePersonRepository.count().await().indefinitely());

        // Check that we called it 4 times
        Mockito.verify(mockablePersonRepository, Mockito.times(4)).count();

        // Mock only with specific parameters
        Person p = new Person();
        Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(Uni.createFrom().item(p));
        Assertions.assertSame(p, mockablePersonRepository.findById(12l).await().indefinitely());
        Assertions.assertNull(mockablePersonRepository.findById(42l).await().indefinitely());

        // Mock throwing
        Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException());
        try {
            mockablePersonRepository.findById(12l);
            Assertions.fail();
        } catch (WebApplicationException x) {
        }

        // We can even mock your custom methods
        Mockito.when(mockablePersonRepository.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList()));
        Assertions.assertTrue(mockablePersonRepository.findOrdered().await().indefinitely().isEmpty());

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

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

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

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

  • 愚蠢的getter和setter方法:由于Java语言中缺乏对属性的支持,我们必须创建字段,然后为这些字段生成getter和setter方法,即便这些方法实际上除了读/写字段外没有其他用处。

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

  • 使用public字段,抛开愚蠢的getter和setter方法。在底层,我们会生成所有缺失的getter和setter方法,并将所有对这些字段的访问重写为getter/setter方法的调用,以使用访问器方法。这样,当你需要的时候,仍可以编写 有用的 访问器,即使实体类的调用者仍然使用字段访问,实际也会使用这些访问器。

  • 使用Active Record模式:把你所有的实体逻辑放在实体类的静态方法中,不要创建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 Panache依赖于编译时对实体类的字节码增强。

Panache通过判断是否存在标记文件 META-INF/panache-archive.marker 来识别jar包是否包含 Panache 实体类(及 Panache 实体类的调用方)。Panache 包含一个注解处理器,它会自动在依赖 Panache(包括间接依赖Panache)的jar包中创建此文件。如果在某些情况下你禁用了注解处理器,可能需要手动创建此文件。

如果你的项目包含 jpa-modelgen 注解处理器,则默认情况下会排除 Panache 注解处理器。这种情况下,你应该自己创建标记文件,或者添加 quarkus-panache-common 插件,如下所示:
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${compiler-plugin.version}</version>
    <configuration>
      <annotationProcessorPaths>
        <annotationProcessorPath>
          <groupId>org.hibernate</groupId>
          <artifactId>hibernate-jpamodelgen</artifactId>
          <version>${hibernate.version}</version>
        </annotationProcessorPath>
        <annotationProcessorPath>
          <groupId>io.quarkus</groupId>
          <artifactId>quarkus-panache-common</artifactId>
          <version>${quarkus.platform.version}</version>
        </annotationProcessorPath>
      </annotationProcessorPaths>
    </configuration>
</plugin>