Simplified Hibernate ORM with Panache
Hibernate ORM 是事实上的 Jakarta Persistence(以前称为 JPA)实现,并为你提供了对象关系映射器的全部广度。它让复杂的映射成为可能,但它并没有让简单和常见的映射变得琐碎。Hibernate ORM 搭配 Panache 专注于让你的实体在 Quarkus 中编写起来变得简单且有趣。
- First: an example
- Solution
- Setting up and configuring Hibernate ORM with Panache
- Solution 1: using the active record pattern
- Solution 2: using the repository pattern
- Writing a Jakarta REST resource
- Advanced Query
- Multiple Persistence Units
- Transactions
- Lock management
- Custom IDs
- Mocking
- How and why we simplify Hibernate ORM mappings
- Defining entities in external projects or jars
First: an example
我们在 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 pattern, 有时也称为实体模式。带 Panache 的 Hibernate 也允许通过 `PanacheRepository`使用更加经典的 repository pattern。 |
Solution
我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。
克隆 Git 存储库: git clone $${quickstarts-base-url}.git
,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。
解决办法位于 hibernate-orm-panache-quickstart
directory。
Setting up and configuring Hibernate ORM with Panache
开始:
-
在
application.properties
中添加您的设置 -
使用 `@Entity`为实体添加注释
-
使实体扩展
PanacheEntity
(如果你正在使用存储库模式,则是可选的)
在构建文件中,添加以下依赖项:
-
带 Panache 扩展的 Hibernate ORM
-
您的 JDBC 驱动程序扩展 (
quarkus-jdbc-postgresql
、quarkus-jdbc-h2
、quarkus-jdbc-mariadb
、……)
<!-- 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>
// 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
Solution 1: using the active record pattern
Defining your entity
要定义一个 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()
访问器,同样,对于字段写入和设置器也是如此。这允许在运行时进行适当的封装,因为所有字段调用都会被相应的 getter/setter 调用替换。
Most useful operations
编写实体后,以下是你能够执行的最常见操作:
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());
}
|
Adding entity methods
在实体本身内部向实体添加自定义查询。这样,你和你同事可以轻松地找到它们,而且查询与操作对象位于同一位置。在你的实体类中将它们作为静态方法添加是 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");
}
}
Solution 2: using the repository pattern
Defining your entity
在使用存储库模式时,您可以将您的实体定义为常规 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,你可以使它们扩展 |
Defining your repository
使用存储库时,你会得到与活动记录模式完全相同的便捷方法,注入到存储库中,通过使它们实现 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();
}
Most useful operations
在编写完存储库后,以下是您将能够执行的最常见操作:
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());
|
文档的其余部分仅展示基于活动记录模式的用法,但请记住,它们也可以与存储库模式一起执行。为了简洁,存储库模式的示例已被省略。 |
Writing a Jakarta REST resource
首先,包含 Quarkus REST(以前称为 RESTEasy Reactive)扩展之一以启用 Jakarta REST 终端,例如,为 Jakarta REST 和 JSON 支持添加 io.quarkus:quarkus-rest-jackson
依赖项。
然后,你可以创建以下资源来创建/读取/更新/删除你的 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();
}
}
谨慎地使用 |
为了更轻松地展示带 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 应用程序时初始化数据库,在 |
之后,你可以看到人员列表并添加新人,如下:
$ 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"}]
如果你将 Person 对象视为 Person<1>,则该对象尚未转换。在这种情况下,在 |
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
Advanced Query
Paging
只有当你的表包含足够小的数据集时,你才应该使用 list
和 stream
方法。对于较大的数据集,可以使用 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
类型具有许多其他方法来处理分页和返回流。
Using a range instead of pages
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)
重新切换到分页。
Sorting
所有接受查询字符串的方法还可以接受以下简化的查询格式:
List<Person> persons = Person.list("order by name,birth");
但是,这些方法还接受可选的 @1 参数,这允许您对排序进行抽象:
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));
@2 类有很多方法用于添加列和指定排序方向或 Null 优先级。
Simplified queries
通常,HQL 查询有这种格式: @3,在末尾采用可选元素。
如果您的选择查询未以 @4、@5 或 @6 开头,我们支持以下的其他格式:
-
@7 将展开为 @8
-
@9(和单个参数)将展开为 @10
-
@11 将展开为 @12
-
@13 将展开为 @14
如果您的更新查询未以 @15 开头,我们支持以下的其他格式:
-
@16 将展开为 @17
-
@18(和单个参数)将展开为 @19
-
@20 将展开为 @21
如果您的删除查询未以 @22 开头,我们支持以下的其他格式:
-
@23 将展开为 @24
-
@25(和单个参数)将展开为 @26
-
@27 将展开为 @28
您还可以在明文中编写您的查询 @29: |
Order.find("select distinct o from Order o left join fetch o.lineItems");
Order.update("update Person set name = 'Mortal' where status = ?", Status.Alive);
Named queries
您可以通过在 (简化的) HQL 查询前缀 # 字符来引用命名查询,而不是 (简化的) HQL 查询。您还可以对 count、update 和 delete 查询使用命名查询。
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 实体类别或其超类之一中定义。
Query parameters
你可以按索引(基于 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
) 传递参数。
Query projection
查询投影可以使用 PanacheQuery
对象上的 project(Class)
方法执行,该对象是由 find()
方法返回的。
你可以使用它来限制数据库返回哪些字段。
Hibernate 将使用 DTO projection 并使用投影类中的属性生成一个 SELECT 子句。这也称为 dynamic instantiation 或 constructor expression,更多信息可以在 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 注解的更多详细信息,请参见 native application tips 页面。 |
2 | 这里我们使用公共字段,但如果你愿意,可以使用私有字段和 getter/setter。 |
3 | 此构造函数将由 Hibernate 使用,它必须是你类中唯一的构造函数,并且所有类属性作为参数。 |
project(Class)
方法的实现使用构造函数的参数名称来构建查询的 select 子句,因此必须将编译器配置为将参数名称存储在已编译类中。如果你正在使用 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);
Multiple Persistence Units
有关多个持久性单元的支持已在 the Hibernate ORM guide 中详细说明。
使用 Panache 时,事情很简单:
-
给定的 Panache 实体只能附加到单个持久性单元。
-
鉴于此,Panache 已提供必要的管道以透明地找到与 Panache 实体关联的正确的
EntityManager
。
Transactions
务必在事务中封装修改数据库的方法(例如 entity.persist()
)。将 CDIB 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);
}
}
Lock management
Panache 提供对数据库锁定使用您的实体/存储库的直接支持,方法是使用 findById(Object, LockModeType)
或 find().withLock(LockModeType)
。
以下示例适用于主动记录模式,但同样适用于存储库。
First: Locking using 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;
}
}
Second: Locking in a 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
注解进行注释。
Custom IDs
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> {
//...
}
Mocking
Using the active record pattern
如果您正在使用主动记录模式,则您无法直接使用 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 上调用您的 verify 和 do* 方法,而不是在 Mockito 上,否则您将不知道要传递哪个模拟对象。 |
Mocking EntityManager
, Session
and entity instance methods
如果您需要模拟实体实例方法(例如 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());
}
}
Using the repository pattern
如果正在使用仓库模式,则可以使用 @5 模块直接使用 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);
}
}
How and why we simplify Hibernate ORM mappings
在编写 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,并重写对这些字段的所有访问以使用访问方法。通过这种方式,当需要 _useful_访问器时仍然可以编写它,即使实体用户仍使用域访问也会使用它。这意味着,从 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 从未变得如此简洁利落。