编辑此页面

简化的 Hibernate ORM with Panache

Hibernate ORM 是事实上的 Jakarta Persistence(前身为 JPA)实现,为您提供了完整的对象关系映射器。 它可以实现复杂的映射,但不能使简单和常见的映射变得简单。 使用 Panache 的 Hibernate ORM 专注于使您的实体在 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) 查询片段并上下文化其余部分。 这使得代码非常简洁但又易于阅读。
上面描述的本质上是 活动记录模式,有时也简称为实体模式。 通过 PanacheRepository,使用 Panache 的 Hibernate 还允许使用更经典的 存储库模式

解决方案

我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。

克隆 Git 存储库:git clone https://github.com/quarkusio/quarkus-quickstarts.git,或者下载 存档

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

设置和配置使用 Panache 的 Hibernate ORM

开始使用

  • 将您的设置添加到 application.properties

  • 使用 @Entity 注释您的实体

  • 使您的实体扩展 PanacheEntity(如果您使用存储库模式,则为可选)

按照 Hibernate 设置指南 进行所有配置。

在您的构建文件中,添加以下依赖项

  • 使用 Panache 扩展的 Hibernate ORM

  • 您的 JDBC 驱动程序扩展(quarkus-jdbc-postgresqlquarkus-jdbc-h2quarkus-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://:5432/mydatabase

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

解决方案 1:使用活动记录模式

定义您的实体

要定义 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;
}

您可以将所有 Jakarta Persistence 列注释放在公共字段上。 如果您需要不持久化某个字段,请在其上使用 @Transient 注释。 如果您需要编写访问器,则可以

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() 访问器,字段写入和 setter 也是如此。 这允许在运行时进行适当的封装,因为所有字段调用都将被相应的 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 方法需要事务才能工作。
由于它们执行 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:使用存储库模式

定义您的实体

使用存储库模式时,您可以将实体定义为常规 Jakarta Persistence 实体。

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 会为您生成它们。 您甚至可以扩展 PanacheEntity 并利用它提供的默认 ID。

定义您的存储库

使用存储库时,您可以通过使它们实现 PanacheRepository 来获得与活动记录模式完全相同的便捷方法,这些方法被注入到您的存储库中

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 上定义的所有操作,因此使用它与使用活动记录模式完全相同,只是您需要注入它

import jakarta.inject.Inject;

@Inject
PersonRepository personRepository;

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

最有用的操作

编写存储库后,以下是您可以执行的最常见的操作

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 方法需要事务才能工作。
本文档的其余部分仅显示基于活动记录模式的用法,但请记住,它们也可以使用存储库模式执行。 为了简洁起见,存储库模式示例已被省略。

编写 Jakarta REST 资源

首先,包含一个 Quarkus REST(以前称为 RESTEasy Reactive)扩展以启用 Jakarta REST 端点,例如,添加 io.quarkus:quarkus-rest-jackson 依赖项以获得 Jakarta REST 和 JSON 支持。

然后,您可以创建以下资源来创建/读取/更新/删除您的 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 注释,您可以为了简单起见在类级别添加该注释。

为了更容易地展示使用 Dev 模式在 Quarkus 上使用 Panache 的 Hibernate ORM 的一些功能,应将一些测试数据插入到数据库中,方法是将以下内容添加到名为 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;
如果您想在生产环境中启动 Quarkus 应用程序时初始化数据库,除了 import.sql 之外,还将 quarkus.hibernate-orm.schema-management.strategy=drop-and-create 添加到 Quarkus 启动选项。

之后,您可以看到人员列表并添加新的人员,如下所示

$ curl -w "\n" https://: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"}' https://:8080/persons

$ curl -w "\n" https://:8080/persons
[{"id":1,"name":"Emily Brown","birth":"1995-09-12","status":"Alive"}, {"id":2,"name":"William Davis","birth":"1988-07-04","status":"Alive"}]
如果您看到 Person 对象为 Person<1>,则该对象尚未转换。 在这种情况下,请在 pom.xml 中添加依赖项 quarkus-rest-jackson
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();

您不能混合使用范围和页面:如果您使用范围,则所有依赖于具有当前页面的方法都会抛出 UnsupportedOperationException; 您可以使用 page(Page)page(int, int) 切换回分页。

排序

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

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

但是这些方法也接受一个可选的 Sort 参数,该参数允许您抽象您的排序

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

Sort 类有很多添加列和指定排序方向或 null 优先顺序的方法。

简化的查询

通常,HQL 查询的形式如下:from EntityName [where …​] [order by …​],末尾带有可选元素。

如果您的选择查询不是以 fromselectwith 开头,我们支持以下其他形式

  • order by …​ 将扩展为 from EntityName order by …​

  • <singleAttribute>(和单个参数)将扩展为 from EntityName where <singleAttribute> = ?

  • where <query> 将扩展为 from EntityName where <query>

  • <query> 将扩展为 from EntityName where <query>

如果您的更新查询不是以 update 开头,我们支持以下其他形式

  • from EntityName …​ 将扩展为 update EntityName …​

  • set? <singleAttribute>(和单个参数)将扩展为 update EntityName set <singleAttribute> = ?

  • set? <update-query> 将扩展为 update EntityName set <update-query>

如果您的删除查询不是以 delete 开头,我们支持以下其他形式

  • from EntityName …​ 将扩展为 delete from EntityName …​

  • <singleAttribute>(和单个参数)将扩展为 delete from EntityName where <singleAttribute> = ?

  • <query> 将扩展为 delete from EntityName where <query>

您还可以用纯 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 查询。 您还可以将命名查询用于计数、更新和删除查询。

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

命名查询只能在您的 Jakarta Persistence 实体类中或其超类之一中定义。

查询参数

您可以按索引(从 1 开始)传递查询参数,如下所示

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

或使用 Map 按名称传递

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 将使用 **DTO 投影** 并生成一个带有投影类属性的 SELECT 子句。 这也称为 **动态实例化** 或 **构造函数表达式**,更多信息可以在 Hibernate 指南中找到:hql select clause

投影类需要有一个包含其所有属性的构造函数,此构造函数将用于实例化投影 DTO,而不是使用实体类。 此类必须具有一个匹配的构造函数,其中所有类属性作为参数。

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 在本机编译期间保留该类及其成员。 有关 @RegisterForReflection 注释的更多详细信息,请参见 本机应用程序技巧 页面。
2 我们这里使用公共字段,但如果您愿意,可以使用私有字段和 getter/setter。
3 Hibernate 将使用此构造函数,它必须是您类中唯一的构造函数,并且具有所有类属性作为参数。

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

如果您运行 Java 17+,则记录非常适合投影类。

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

如果您想在具有嵌套类的类中投影一个实体,您可以在这些嵌套类上使用 @NestedProjectedClass 注释。

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

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

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

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

PanacheQuery<DogDto> query = Dog.findAll().project(DogDto.class);
1 当您想投影 @Embedded 实体或 @ManyToOne@OneToOne 关系时,可以使用此注释。 它不支持 @OneToMany@ManyToMany 关系。

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

import io.quarkus.runtime.annotations.RegisterForReflection;

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

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

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

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

不可能同时具有 HQL select new 查询和 .project(Class) - 您需要选择一种方法。

例如,这将失败

PanacheQuery<RaceWeight> query = Person.find("select new MyView(d.race, AVG(d.weight)) from Dog d group by d.race").project(AnotherView.class);

多个持久性单元

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

使用 Panache 时,事情很简单

  • 给定的 Panache 实体只能附加到单个持久性单元。

  • 鉴于此,Panache 已经提供了必要的管道来透明地找到与 Panache 实体关联的适当 EntityManager

事务

确保将修改数据库的方法(例如 entity.persist())包装在事务中。 标记 CDI bean 方法 @Transactional 将为您完成此操作,并使该方法成为事务边界。 我们建议在您的应用程序入口点边界(如您的 REST 端点控制器)处这样做。

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

以下是刷新方法用法的示例,以便在发生 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 使用 findById(Object, LockModeType)find().withLock(LockModeType) 提供对数据库锁定的直接支持。

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

首先:使用 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 声明为公共字段

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;

    //...
}

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

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

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

模拟

使用活动记录模式

如果您使用的是活动记录模式,则无法直接使用 Mockito,因为它不支持模拟静态方法,但您可以使用 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 而不是 Mockito 上调用您的 verifydo* 方法,否则您将不知道要传递什么模拟对象。

模拟 EntityManagerSession 和实体实例方法

如果您需要模拟实体实例方法,例如 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());
    }
}

使用存储库模式

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

和此存储库

@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 实体时,用户已经习惯于不情愿地处理许多令人讨厌的事情,例如

  • 重复 ID 逻辑:大多数实体都需要 ID,大多数人不在乎它是如何设置的,因为它与您的模型没有真正的关系。

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

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

  • Hibernate 是非常通用的,但并没有使执行构成我们模型使用量 90% 的简单操作变得简单。

使用 Panache,我们采用了自以为是的策略来解决所有这些问题

  • 让您的实体扩展 PanacheEntity:它具有自动生成的 ID 字段。 如果您需要自定义 ID 策略,您可以扩展 PanacheEntityBase 并自己处理 ID。

  • 使用公共字段。 摆脱愚蠢的 getter 和 setter。 无需 Panache 的 Hibernate ORM 也不要求您使用 getter 和 setter,但 Panache 还会生成所有缺失的 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 中定义实体

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

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

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

相关内容