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

使用Panache简化Hibernate ORM

Hibernate ORM是事实上的JPA实现,提供了全方位的对象关系映射(Object Relational Mapper,简称ORM)支持。它支持编写复杂的映射,但编写简单常用的映射却不够简便。Panache专注于简化Hibernate ORM实体,让你的Quarkus开发有趣。

首先看一个例子

Panache允许这样编写Hibernate ORM实体:

public enum Status {
    Alive,
    Deceased
}

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

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

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

    public static void deleteStefs(){
        delete("name", "Stef");
    }
}

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

list() 方法一开始可能会让人吃惊。它只需要接收HQL(JP-QL)查询语句的片段,并对查询语句其余部分进行上下文推断处理。这使得代码非常简明,但也不失可读性。
上面所描述的编码模式本质上是 Active Record模式 ,有时也称为实体模式。Panache也支持通过 PanacheRepository 使用更经典的 Repository模式

解决方案

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

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

该解决方案位于 hibernate-orm-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 ORM中配置Panache

按以下步骤开始:

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

  • 给实体类增加 @Entity 注解

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

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

在你的项目构建文件中,添加以下依赖:

  • Hibernate ORM with Panache扩展

  • JDBC驱动扩展 (如 quarkus-jdbc-postgresql , quarkus-jdbc-h2 , quarkus-jdbc-mariadb , …​)

pom.xml
<!-- Hibernate ORM specific dependencies -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>

<!-- JDBC driver dependencies -->
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
build.gradle
// Hibernate ORM specific dependencies
implementation("io.quarkus:quarkus-hibernate-orm-panache")

// JDBC driver dependencies
implementation("io.quarkus:quarkus-jdbc-postgresql")

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

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

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

解决方案1:使用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
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
    person.delete();
}

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

// finding a specific person by ID
person = Person.findById(personId);

// finding a specific person by ID via an Optional
Optional<Person> optional = Person.findByIdOptional(personId);
person = optional.orElseThrow(() -> new NotFoundException());

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

// counting all persons
long countAll = Person.count();

// counting all living persons
long countAlive = Person.count("status", Status.Alive);

// delete all living persons
Person.delete("status", Status.Alive);

// delete all persons
Person.deleteAll();

// delete by id
boolean deleted = Person.deleteById(personId);

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

上述所有 list 方法都有等效的的 stream 版本。

try (Stream<Person> persons = Person.streamAll()) {
    List<String> namesButEmmanuels = persons
        .map(p -> p.name.toLowerCase() )
        .filter( n -> ! "emmanuel".equals(n) )
        .collect(Collectors.toList());
}
stream 方法的运行需要事务。+ 由于这些stream开头的方法会执行I/O操作,所以应该通过 close() 方法或try-with-resource来关闭底层的 ResultSet 。否则你会看到来自Agroal的警告,它会为你关闭底层的 ResultSet

添加实体方法

你可以在实体类中添加对应实体的自定义查询。这样,查询与查询所操作的实体对象同在一处,你和你的同事可以很容易地找到它们。Panache的Active Record模式推荐将这些方法以静态方法的形式加入实体类。

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

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

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

    public static void deleteStefs(){
        delete("name", "Stef");
    }
}

解决方案2:使用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 Person findByName(String name){
       return find("name", name).firstResult();
   }

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

   public void deleteStefs(){
       delete("name", "Stef");
  }
}

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

@Inject
PersonRepository personRepository;

@GET
public 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
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
    personRepository.delete(person);
}

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

// finding a specific person by ID
person = personRepository.findById(personId);

// finding a specific person by ID via an Optional
Optional<Person> optional = personRepository.findByIdOptional(personId);
person = optional.orElseThrow(() -> new NotFoundException());

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

// counting all persons
long countAll = personRepository.count();

// counting all living persons
long countAlive = personRepository.count("status", Status.Alive);

// delete all living persons
personRepository.delete("status", Status.Alive);

// delete all persons
personRepository.deleteAll();

// delete by id
boolean deleted = personRepository.deleteById(personId);

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

上述所有 list 方法都有等效的的 stream 版本。

Stream<Person> persons = personRepository.streamAll();
List<String> namesButEmmanuels = persons
    .map(p -> p.name.toLowerCase() )
    .filter( n -> ! "emmanuel".equals(n) )
    .collect(Collectors.toList());
stream 方法的运行需要事务。
后续的文档只展示了基于Active Record模式的用法,但请记住,这些用法也可以用于Repository模式。为了简洁起见,省略了Repository模式的例子。

编写一个JAX-RS资源

首先,请添加任意一个RESTEasy Reactive扩展的依赖,以启用JAX-RS端点,例如,添加支持JAX-RS和JSON的 io.quarkus:quarkus-resteasy-reactive-jackson 依赖。

然后,创建以下资源类,用于创建、读取、更新、删除Person实体。

@Path("/persons")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PersonResource {

    @GET
    public List<Person> list() {
        return Person.listAll();
    }

    @GET
    @Path("/{id}")
    public Person get(Long id) {
        return Person.findById(id);
    }

    @POST
    @Transactional
    public Response create(Person person) {
        person.persist();
        return Response.created(URI.create("/persons/" + person.id)).build();
    }

    @PUT
    @Path("/{id}")
    @Transactional
    public Person update(Long id, Person person) {
        Person entity = Person.findById(id);
        if(entity == null) {
            throw new NotFoundException();
        }

        // map all fields from the person parameter to the existing entity
        entity.name = person.name;

        return entity;
    }

    @DELETE
    @Path("/{id}")
    @Transactional
    public void delete(Long id) {
        Person entity = Person.findById(id);
        if(entity == null) {
            throw new NotFoundException();
        }
        entity.delete();
    }

    @GET
    @Path("/search/{name}")
    public Person search(String name) {
        return Person.findByName(name);
    }

    @GET
    @Path("/count")
    public Long count() {
        return Person.count();
    }
}
请注意,在修改数据库的操作上需要使用 @Transactional 注解,为了简单起见,你可以给类添加这个注解。

高级查询

分页

如果你的表数据量很小,你应该只用到 liststream 方法。对于较大的数据集,你可以使用对应的 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
List<Person> firstPage = livingPersons.list();

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

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

// get the number of pages
int numberOfPages = livingPersons.pageCount();

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

// and you can chain methods of course
return Person.find("status", Status.Alive)
    .page(Page.ofSize(25))
    .nextPage()
    .stream()

PanacheQuery 类还有很多其他方法来做分页查询、返回流。

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

PanacheQuery 也支持基于范围的查询。

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

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

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

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

但不能混用范围查询和分页查询。当你使用范围查询时,调用任意依赖于分页查询的方法都会抛出一个 UnsupportedOperationException 异常;你可以使用 page(Page)page(int, int) 方法切换回分页查询模式。

排序

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

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

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

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

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

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

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 Person findByName(String name){
        return find("#Person.getByName", name).firstResult();
    }

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

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

    public static long deleteById(long id) {
        return delete("#Person.deleteById", id);
    }
}

命名查询只能在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属性加载。

多个持久化单元

Hibernate ORM指南 中对多个持久化单元的支持有详细描述。

使用Panache时,事情很简单:

  • 一个Panache实体类只能配置到一个持久化单元。

  • 鉴于此,Panache已经提供了必要的管道, 可以透明地找到Panache实体类对应的 EntityManager

事务

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

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

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

@Transactional
public void create(Parameter parameter){
    try {
        //Here I use the persistAndFlush() shorthand method on a Panache repository to persist to database then flush the changes.
        return parameterRepository.persistAndFlush(parameter);
    }
    catch(PersistenceException pe){
        LOG.error("Unable to create the parameter", pe);
        //in case of error, I save it to disk
        diskPersister.save(parameter);
    }
}

锁管理

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

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

第一:通过findById()方法使用数据库锁。

public class PersonEndpoint {

    @GET
    @Transactional
    public Person findByIdForUpdate(Long id){
        Person p = Person.findById(id, LockModeType.PESSIMISTIC_WRITE);
        //do something useful, the lock will be released when the transaction ends.
        return person;
    }

}

第二:通过find()方法使用数据库锁。

public class PersonEndpoint {

    @GET
    @Transactional
    public Person findByNameForUpdate(String name){
        Person p = Person.find("name", name).withLock(LockModeType.PESSIMISTIC_WRITE).findOne();
        //do something useful, the lock will be released when the transaction ends.
        return person;
    }

}

请注意,事务结束时锁会被释放,所以调用了带锁查询的方法必须加上 @Transactional 注解。

自定义ID

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

你可以通过继承 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>

编写一个简单的实体类:

@Entity
public class Person extends PanacheEntity {

    public String name;

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

        // Now let's specify the return value
        Mockito.when(Person.count()).thenReturn(23L);
        Assertions.assertEquals(23, Person.count());

        // Now let's change the return value
        Mockito.when(Person.count()).thenReturn(42L);
        Assertions.assertEquals(42, Person.count());

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

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

        // Mock only with specific parameters
        Person p = new Person();
        Mockito.when(Person.findById(12L)).thenReturn(p);
        Assertions.assertSame(p, Person.findById(12L));
        Assertions.assertNull(Person.findById(42L));

        // Mock throwing
        Mockito.when(Person.findById(12L)).thenThrow(new WebApplicationException());
        Assertions.assertThrows(WebApplicationException.class, () -> Person.findById(12L));

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

        // Mocking a void method
        Person.voidMethod();

        // Make it throw
        PanacheMock.doThrow(new RuntimeException("Stef2")).when(Person.class).voidMethod();
        try {
            Person.voidMethod();
            Assertions.fail();
        } catch (RuntimeException x) {
            Assertions.assertEquals("Stef2", x.getMessage());
        }

        // Back to doNothing
        PanacheMock.doNothing().when(Person.class).voidMethod();
        Person.voidMethod();

        // Make it call the real method
        PanacheMock.doCallRealMethod().when(Person.class).voidMethod();
        try {
            Person.voidMethod();
            Assertions.fail();
        } catch (RuntimeException x) {
            Assertions.assertEquals("void", x.getMessage());
        }

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

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

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

@QuarkusTest
public class PanacheMockingTest {

    @InjectMock
    Session session;

    @BeforeEach
    public void setup() {
        Query mockQuery = Mockito.mock(Query.class);
        Mockito.doNothing().when(session).persist(Mockito.any());
        Mockito.when(session.createQuery(Mockito.anyString())).thenReturn(mockQuery);
        Mockito.when(mockQuery.getSingleResult()).thenReturn(0l);
    }

    @Test
    public void testPanacheMocking() {
        Person p = new Person();
        // mocked via EntityManager mocking
        p.persist();
        Assertions.assertNull(p.id);

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

使用Repository模式

如果你使用Repository模式,你可以直接使用Mockito。使用 quarkus-junit5-mockito 模块可以更简单地模拟Bean:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
    <scope>test</scope>
</dependency>

编写一个简单的实体类:

@Entity
public class Person {

    @Id
    @GeneratedValue
    public Long id;

    public String name;
}

以及这个Repository类:

@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {
    public 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, personRepository.count());

        // Now let's specify the return value
        Mockito.when(personRepository.count()).thenReturn(23L);
        Assertions.assertEquals(23, personRepository.count());

        // Now let's change the return value
        Mockito.when(personRepository.count()).thenReturn(42L);
        Assertions.assertEquals(42, personRepository.count());

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

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

        // Mock only with specific parameters
        Person p = new Person();
        Mockito.when(personRepository.findById(12L)).thenReturn(p);
        Assertions.assertSame(p, personRepository.findById(12L));
        Assertions.assertNull(personRepository.findById(42L));

        // Mock throwing
        Mockito.when(personRepository.findById(12L)).thenThrow(new WebApplicationException());
        Assertions.assertThrows(WebApplicationException.class, () -> personRepository.findById(12L));

        Mockito.when(personRepository.findOrdered()).thenReturn(Collections.emptyList());
        Assertions.assertTrue(personRepository.findOrdered().isEmpty());

        // We can even mock your custom methods
        Mockito.verify(personRepository).findOrdered();
        Mockito.verify(personRepository, Mockito.atLeastOnce()).findById(Mockito.any());
        Mockito.verifyNoMoreInteractions(personRepository);
    }
}

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

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

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

  • Traditional EE patterns advise to split entity definition (the model) from the operations you can do on them (DAOs, Repositories), but really that requires a 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方法。Hibernate ORM Panache不要求你使用getter和setter方法,但Panache会额外生成所有缺失的getter和setter方法,并将所有对这些字段的访问重写为getter/setter方法的调用。这样,当你需要的时候,仍可以编写 有用的 访问器,即使实体类的调用者仍然使用字段访问,实际也会使用这些访问器。这意味着从Hibernate的角度来看,你正在通过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 ORM看起来变得如此轻量和整洁。

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

Hibernate ORM 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>