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 is the de facto Jakarta Persistence (formerly known as JPA) implementation and offers you the full breadth of an Object Relational Mapper. It makes complex mappings possible, but it does not make simple and common mappings trivial. Hibernate ORM with Panache focuses on making your entities trivial and fun to write in Quarkus.

首先看一个例子

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

package org.acme;

public enum Status {
    Alive,
    Deceased
}
package org.acme;

import java.time.LocalDate;
import java.util.List;
import jakarta.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;

@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 ,或者下载 压缩包

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

在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 注解,并将数据库列作为公共字段添加到实体类:

package org.acme;

import java.time.LocalDate;
import java.util.List;
import jakarta.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;

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

package org.acme;

import java.time.LocalDate;
import java.util.List;
import jakarta.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;

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

常用操作

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

import java.time.LocalDate;
import java.time.Month;
import java.util.List;
import java.util.Optional;

// 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 is 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 版本。

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.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 方式。

package org.acme;

import java.time.LocalDate;
import java.util.List;
import jakarta.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;

@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模式

定义实体类

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

package org.acme;

import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.time.LocalDate;

@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模式下完全相同的便捷方法:

package org.acme;

import io.quarkus.hibernate.orm.panache.PanacheRepository;

import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;

@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类的实例:

import jakarta.inject.Inject;

@Inject
PersonRepository personRepository;

@GET
public long count(){
    return personRepository.count();
}

常用操作

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

import java.time.LocalDate;
import java.time.Month;
import java.util.List;
import java.util.Optional;

// 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 is 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 版本。

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Stream<Person> persons = personRepository.streamAll();
List<String> namesButEmmanuels = persons
    .map(p -> p.name.toLowerCase() )
    .filter( n -> ! "emmanuel".equals(n) )
    .collect(Collectors.toList());
stream 方法的运行需要事务。
其余的文档只展示了基于活动记录模式的用法,但请记住,这些用法也可以用资源库模式来执行。为了简洁起见,已省略存储库模式示例。

Writing a Jakarta REST resource

First, include one of the Quarkus REST (formerly RESTEasy Reactive) extensions to enable Jakarta REST endpoints, for example, add the io.quarkus:quarkus-rest-jackson dependency for Jakarta REST and JSON support.

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

package org.acme;

import java.net.URI;
import java.util.List;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

@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 注解,为了简单起见,你可以给类添加这个注解。

To make it easier to showcase some capabilities of Hibernate ORM with Panache on Quarkus with Dev mode, some test data should be inserted into the database by adding the following content to a new file named src/main/resources/import.sql:

INSERT INTO person (id, birth, name, status) VALUES (1, '1995-09-12', 'Emily Brown', 0);
ALTER SEQUENCE person_seq RESTART WITH 2;
If you would like to initialize the DB when you start the Quarkus app in your production environment, add quarkus.hibernate-orm.database.generation=drop-and-create to the Quarkus startup options in addition to import.sql.

After that, you can see the people list and add new person as followings:

$ curl -w "\n" http://localhost:8080/persons
[{"id":1,"name":"Emily Brown","birth":"1995-09-12","status":"Alive"}]

$ curl -X POST -H "Content-Type: application/json" -d '{"name" : "William Davis" , "birth" : "1988-07-04", "status" : "Alive"}' http://localhost:8080/persons

$ curl -w "\n" http://localhost:8080/persons
[{"id":1,"name":"Emily Brown","birth":"1995-09-12","status":"Alive"}, {"id":2,"name":"William Davis","birth":"1988-07-04","status":"Alive"}]
If you see the Person object as Person<1>, then the object has not been converted. In this case, add the dependency quarkus-rest-jackson in pom.xml.
pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-jackson</artifactId>
</dependency>

高级查询

分页

如果你的表数据量很小,你应该只用到 liststream 方法。对于较大的数据集,你可以使用对应的 find 方法,它返回一个 PanacheQuery ,可以对其进行分页查询操作:

import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.panache.common.Page;
import java.util.List;

// 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 也支持基于范围的查询。

import io.quarkus.hibernate.orm.panache.PanacheQuery;
import java.util.List;

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

你不能混合使用ranges和pages:如果你使用range,所有依赖于拥有当前页面的方法将抛出一个 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:

import io.quarkus.panache.common.Sort;

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 …​​] ,结尾处有可选元素。

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 Person set name = 'Mortal' where status = ?", Status.Alive);

命名查询

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

package org.acme;

import java.time.LocalDate;
import jakarta.persistence.Entity;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.panache.common.Parameters;

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

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

查询参数

你可以通过索引(从1开始)传递查询参数,如下所示:

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

或者使用 Map ,用key做参数名:

import java.util.HashMap;
import java.util.Map;

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

或者使用 Parameters 类,也可以方便地构造一个 Map

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

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

所有查询操作都可以接收按索引( Object…​​ )或按名称( Map<String,Object>Parameters )传递的参数。

查询结果投影

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

The projection class needs to have a constructor that contains all its attributes, this constructor will be used to instantiate the projection DTO instead of using the entity class. This class must have a matching constructor with all the class attributes as parameters.

import io.quarkus.runtime.annotations.RegisterForReflection;
import io.quarkus.hibernate.orm.panache.PanacheQuery;

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

If you run Java 17+, records are a good fit for projection classes.

如果DTO投影对象中有来自引用的实体字段,可以使用 @ProjectedFieldName 注解指定SELECT语句使用的查询路径。

import jakarta.persistence.ManyToOne;
import io.quarkus.hibernate.orm.panache.common.ProjectedFieldName;

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

多个持久化单元

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

使用Panache时,事情很简单:

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

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

事务

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

Hibernate ORM 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 ORM sends 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 异常时执行指定操作:

import jakarta.persistence.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()方法使用数据库锁。

import jakarta.persistence.LockModeType;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.GET;

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()方法使用数据库锁。

import jakarta.persistence.LockModeType;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.GET;

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字段:

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;

@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字段类型作为额外的类型参数:

import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;

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

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

import io.quarkus.panache.mock.PanacheMock;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import jakarta.ws.rs.WebApplicationException;
import java.util.Collections;

@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 对象来实现:

import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import org.hibernate.Session;
import org.hibernate.query.SelectionQuery;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

@QuarkusTest
public class PanacheMockingTest {

    @InjectMock
    Session session;

    @BeforeEach
    public void setup() {
        SelectionQuery mockQuery = Mockito.mock(SelectionQuery.class);
        Mockito.doNothing().when(session).persist(Mockito.any());
        Mockito.when(session.createSelectionQuery(Mockito.anyString(), Mockito.any())).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模式

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

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

import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import jakarta.ws.rs.WebApplicationException;
import java.util.Collections;

@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实体类时,用户已经习惯了被迫处理许多烦人的事情,例如:

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

  • 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方法使用访问器,即使代码看起来像字段访问器。

  • 使用活动记录模式:把你所有的实体逻辑放在实体类的静态方法中,不要创建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 with Panache in Quarkus 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