简化的 Hibernate Reactive with Panache
Hibernate Reactive 是唯一支持响应式 Jakarta Persistence(以前称为 JPA)的实现,它为您提供了对象关系映射器的全部功能,允许您通过响应式驱动程序访问数据库。它可以实现复杂的映射,但并不能使简单和常见的映射变得微不足道。使用 Panache 的 Hibernate Reactive 专注于让您在 Quarkus 中轻松有趣地编写实体。
Hibernate Reactive 不是 Hibernate ORM 的替代品,也不是 Hibernate ORM 的未来。它是一个为需要高并发的响应式用例量身定制的不同技术栈。 此外,使用 Quarkus REST(以前称为 RESTEasy Reactive,我们的默认 REST 层)不需要使用 Hibernate Reactive。将 Quarkus REST 与 Hibernate ORM 结合使用是完全可行的,如果您不需要高并发,或者不习惯响应式范式,则建议使用 Hibernate ORM。 |
首先:一个例子
我们在 Panache 中所做的就是让您可以这样编写 Hibernate Reactive 实体
import io.quarkus.hibernate.reactive.panache.PanacheEntity;
@Entity
public class Person extends PanacheEntity {
public String name;
public LocalDate birth;
public Status status;
public static Uni<Person> findByName(String name){
return find("name", name).firstResult();
}
public static Uni<List<Person>> findAlive(){
return list("status", Status.Alive);
}
public static Uni<Long> deleteStefs(){
return delete("name", "Stef");
}
}
您是否注意到代码更加简洁易读?这看起来很有趣吗?继续阅读!
list() 方法乍一看可能会让人感到意外。它接受 HQL(JP-QL)查询的片段,并将其余部分进行上下文化。这使得代码非常简洁且易于阅读。 |
解决方案
我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。
克隆 Git 存储库:git clone https://github.com/quarkusio/quarkus-quickstarts.git
,或者下载一个归档。
解决方案位于hibernate-reactive-panache-quickstart
目录中。
设置和配置 Hibernate Reactive with Panache
开始使用
-
将您的设置添加到
application.properties
中 -
使用
@Entity
注解您的实体 -
让您的实体继承
PanacheEntity
(如果您使用存储库模式,则可选)
在您的pom.xml
中,添加以下依赖项
-
Hibernate Reactive with Panache 扩展
-
您的响应式驱动程序扩展(
quarkus-reactive-pg-client
、quarkus-reactive-mysql-client
、quarkus-reactive-db2-client
,…)
例如
<!-- Hibernate Reactive dependency -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-reactive-panache</artifactId>
</dependency>
<!-- Reactive SQL client for PostgreSQL -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
// Hibernate Reactive dependency
implementation("io.quarkus:quarkus-hibernate-reactive-panache")
Reactive SQL client for PostgreSQL
implementation("io.quarkus:quarkus-reactive-pg-client")
然后将相关的配置属性添加到 application.properties
中。
# configure your datasource
quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = sarah
quarkus.datasource.password = connor
quarkus.datasource.reactive.url = vertx-reactive:postgresql://: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
注解它,并将您的列添加为公共字段
@Entity
public class Person extends PanacheEntity {
public String name;
public LocalDate birth;
public Status status;
}
您可以将所有 Jakarta Persistence 列注解放在公共字段上。如果某个字段不需要持久化,请在其上使用@Transient
注解。如果您需要编写访问器,您可以这样做
@Entity
public class Person extends PanacheEntity {
public String name;
public LocalDate birth;
public Status status;
// return name as uppercase in the model
public String getName(){
return name.toUpperCase();
}
// store all names in lowercase in the DB
public void setName(String name){
this.name = name.toLowerCase();
}
}
得益于我们的字段访问重写,当您的用户读取person.name
时,它们实际上会调用您的getName()
访问器,同样对于字段写入和 setter。这允许在运行时进行适当的封装,因为所有字段访问都将被替换为相应的 getter/setter 调用。
最有用的操作
编写实体后,以下是您可以执行的最常见的操作
// creating a person
Person person = new Person();
person.name = "Stef";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;
// persist it
Uni<Void> persistOperation = person.persist();
// note that once persisted, you don't need to explicitly save your entity: all
// modifications are automatically persisted on transaction commit.
// check if it is persistent
if(person.isPersistent()){
// delete it
Uni<Void> deleteOperation = person.delete();
}
// getting a list of all Person entities
Uni<List<Person>> allPersons = Person.listAll();
// finding a specific person by ID
Uni<Person> personById = Person.findById(23L);
// finding all living persons
Uni<List<Person>> livingPersons = Person.list("status", Status.Alive);
// counting all persons
Uni<Long> countAll = Person.count();
// counting all living persons
Uni<Long> countAlive = Person.count("status", Status.Alive);
// delete all living persons
Uni<Long> deleteAliveOperation = Person.delete("status", Status.Alive);
// delete all persons
Uni<Long> deleteAllOperation = Person.deleteAll();
// delete by id
Uni<Boolean> deleteByIdOperation = Person.deleteById(23L);
// set the name of all living persons to 'Mortal'
Uni<Integer> updateOperation = Person.update("name = 'Mortal' where status = ?1", Status.Alive);
添加实体方法
在实体内部添加实体上的自定义查询。这样,您和您的同事可以轻松找到它们,并且查询与它们操作的对象位于同一位置。将它们添加为实体类中的静态方法是 Panache Active Record 的方式。
@Entity
public class Person extends PanacheEntity {
public String name;
public LocalDate birth;
public Status status;
public static Uni<Person> findByName(String name){
return find("name", name).firstResult();
}
public static Uni<List<Person>> findAlive(){
return list("status", Status.Alive);
}
public static Uni<Long> deleteStefs(){
return delete("name", "Stef");
}
}
解决方案 2:使用存储库模式
定义您的实体
使用存储库模式时,您可以将实体定义为常规 Jakarta Persistence 实体。
@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
,您可以在存储库中获得与活动记录模式完全相同的便捷方法
@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {
// put your custom logic here as instance methods
public Uni<Person> findByName(String name){
return find("name", name).firstResult();
}
public Uni<List<Person>> findAlive(){
return list("status", Status.Alive);
}
public Uni<Long> deleteStefs(){
return delete("name", "Stef");
}
}
在您的存储库中可以使用 PanacheEntityBase
上定义的所有操作,因此使用它与使用活动记录模式完全相同,只是您需要注入它
@Inject
PersonRepository personRepository;
@GET
public Uni<Long> count(){
return personRepository.count();
}
最有用的操作
编写存储库后,以下是您可以执行的最常见的操作
// creating a person
Person person = new Person();
person.setName("Stef");
person.setBirth(LocalDate.of(1910, Month.FEBRUARY, 1));
person.setStatus(Status.Alive);
// persist it
Uni<Void> persistOperation = personRepository.persist(person);
// note that once persisted, you don't need to explicitly save your entity: all
// modifications are automatically persisted on transaction commit.
// check if it is persistent
if(personRepository.isPersistent(person)){
// delete it
Uni<Void> deleteOperation = personRepository.delete(person);
}
// getting a list of all Person entities
Uni<List<Person>> allPersons = personRepository.listAll();
// finding a specific person by ID
Uni<Person> personById = personRepository.findById(23L);
// finding all living persons
Uni<List<Person>> livingPersons = personRepository.list("status", Status.Alive);
// counting all persons
Uni<Long> countAll = personRepository.count();
// counting all living persons
Uni<Long> countAlive = personRepository.count("status", Status.Alive);
// delete all living persons
Uni<Long> deleteLivingOperation = personRepository.delete("status", Status.Alive);
// delete all persons
Uni<Long> deleteAllOperation = personRepository.deleteAll();
// delete by id
Uni<Boolean> deleteByIdOperation = personRepository.deleteById(23L);
// set the name of all living persons to 'Mortal'
Uni<Integer> updateOperation = personRepository.update("name = 'Mortal' where status = ?1", Status.Alive);
本文档的其余部分仅显示基于活动记录模式的用法,但请记住,它们也可以使用存储库模式执行。 为了简洁起见,存储库模式示例已被省略。 |
高级查询
分页
您应该仅在表包含足够小的数据集时使用list
方法。对于更大的数据集,您可以使用find
方法的等效项,它们会返回一个PanacheQuery
,您可以在其上进行分页
// create a query for all living persons
PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);
// make it use pages of 25 entries at a time
livingPersons.page(Page.ofSize(25));
// get the first page
Uni<List<Person>> firstPage = livingPersons.list();
// get the second page
Uni<List<Person>> secondPage = livingPersons.nextPage().list();
// get page 7
Uni<List<Person>> page7 = livingPersons.page(Page.of(7, 25)).list();
// get the number of pages
Uni<Integer> numberOfPages = livingPersons.pageCount();
// get the total number of entities returned by this query without paging
Uni<Long> count = livingPersons.count();
// and you can chain methods of course
Uni<List<Person>> persons = Person.find("status", Status.Alive)
.page(Page.ofSize(25))
.nextPage()
.list();
PanacheQuery
类型还有许多其他方法来处理分页和返回流。
使用范围而不是页面
PanacheQuery
还允许基于范围的查询。
// create a query for all living persons
PanacheQuery<Person> livingPersons = Person.find("status", Status.Alive);
// make it use a range: start at index 0 until index 24 (inclusive).
livingPersons.range(0, 24);
// get the range
Uni<List<Person>> firstRange = livingPersons.list();
// to get the next range, you need to call range again
Uni<List<Person>> secondRange = livingPersons.range(25, 49).list();
您不能混合使用范围和页面:如果您使用范围,则所有依赖于具有当前页面的方法都会抛出 |
排序
所有接受查询字符串的方法也接受以下简化查询形式
Uni<List<Person>> persons = Person.list("order by name,birth");
但这些方法还接受一个可选的Sort
参数,该参数允许您抽象排序
Uni<List<Person>> persons = Person.list(Sort.by("name").and("birth"));
// and with more restrictions
Uni<List<Person>> persons = Person.list("status", Sort.by("name").and("birth"), Status.Alive);
// and list first the entries with null values in the field "birth"
Uni<List<Person>> persons = Person.list(Sort.by("birth", Sort.NullPrecedence.NULLS_FIRST));
Sort
类有许多用于添加列和指定排序方向或 null 优先顺序的方法。
简化的查询
通常,HQL 查询的形式为:from EntityName [where …] [order by …]
,最后是可选元素。
如果您的 select 查询不以from
、select
或with
开头,我们支持以下附加形式
-
order by …
,它将展开为from EntityName order by …
-
<singleAttribute>
(和单个参数),它将展开为from EntityName where <singleAttribute> = ?
-
where <query>
将展开为from EntityName where <query>
-
<query>
将展开为from EntityName where <query>
如果您的 update 查询不以update
开头,我们支持以下附加形式
-
from EntityName …
,它将展开为update EntityName …
-
set? <singleAttribute>
(和单个参数),它将展开为update EntityName set <singleAttribute> = ?
-
set? <update-query>
将展开为update EntityName set <update-query>
如果您的 delete 查询不以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 from Person set name = 'Mortal' where status = ?", Status.Alive);
命名查询
您可以通过在名称前加上 '#' 字符来引用命名查询,而不是(简化的)HQL 查询。您也可以为 count、update 和 delete 查询使用命名查询。
@Entity
@NamedQueries({
@NamedQuery(name = "Person.getByName", query = "from Person where name = ?1"),
@NamedQuery(name = "Person.countByStatus", query = "select count(*) from Person p where p.status = :status"),
@NamedQuery(name = "Person.updateStatusById", query = "update Person p set p.status = :status where p.id = :id"),
@NamedQuery(name = "Person.deleteById", query = "delete from Person p where p.id = ?1")
})
public class Person extends PanacheEntity {
public String name;
public LocalDate birth;
public Status status;
public static Uni<Person> findByName(String name){
return find("#Person.getByName", name).firstResult();
}
public static Uni<Long> countByStatus(Status status) {
return count("#Person.countByStatus", Parameters.with("status", status).map());
}
public static Uni<Long> updateStatusById(Status status, Long id) {
return update("#Person.updateStatusById", Parameters.with("status", status).and("id", id));
}
public static Uni<Long> deleteById(Long id) {
return delete("#Person.deleteById", id);
}
}
命名查询只能在您的 Jakarta Persistence 实体类内或其某个超类上定义。 |
查询参数
您可以按索引(从 1 开始)传递查询参数,如下所示
Person.find("name = ?1 and status = ?2", "stef", Status.Alive);
或使用 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 子句
投影类必须是一个有效的 Java Bean,并有一个包含其所有属性的构造函数,此构造函数将用于实例化投影 DTO,而不是使用实体类。这必须是该类的唯一构造函数。
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection (1)
public class PersonName {
public final String name; (2)
public PersonName(String name){ (3)
this.name = name;
}
}
// only 'name' will be loaded from the database
PanacheQuery<PersonName> query = Person.find("status", Status.Alive).project(PersonName.class);
1 | @RegisterForReflection 注解指示 Quarkus 在本地编译期间保留类及其成员。有关@RegisterForReflection 注解的更多详细信息,请参阅本地应用程序提示页面。 |
2 | 我们在这里使用公共字段,但如果您愿意,也可以使用私有字段和 getter/setter。 |
3 | 此构造函数将被 Hibernate 使用,并且必须有一个具有所有类属性作为参数的匹配构造函数。 |
|
如果 DTO 投影对象中有一个来自关联实体的字段,您可以使用@ProjectedFieldName
注解为 SELECT 语句提供路径。
@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 Reactive 将使用此构造函数。当查询具有 select 子句时,可能存在多个构造函数。 |
不能同时使用 HQL 例如,这将失败
|
会话和事务
首先,Panache 实体的多数方法必须在响应式Mutiny.Session
的范围内调用。在某些情况下,会话会在需要时自动打开。例如,如果 Panache 实体方法在包含quarkus-rest
扩展的应用程序的 Jakarta REST 资源方法中调用。对于其他情况,有声明式和命令式两种方法来确保会话被打开。您可以注解一个返回Uni
的 CDI 业务方法@WithSession
。该方法将被拦截,并且返回的Uni
将在响应式会话的范围内触发。或者,您可以使用Panache.withSession()
方法达到相同的效果。
请注意,Panache 实体不能从阻塞线程调用。另请参阅响应式入门指南,其中解释了 Quarkus 中响应式原则的基础知识。 |
此外,请确保将修改数据库或涉及多个查询(例如entity.persist()
)的方法包装在事务中。您可以注解一个返回Uni
的 CDI 业务方法@WithTransaction
。该方法将被拦截,并且返回的Uni
将在事务边界内触发。或者,您可以使用Panache.withTransaction()
方法达到相同的效果。
您不能在 Hibernate Reactive 中使用@Transactional 注解进行事务:您必须使用@WithTransaction ,并且您注解的方法必须返回Uni 才能实现非阻塞。 |
Hibernate Reactive 会将您对实体的更改分批处理,并在事务结束时或查询之前发送更改(称为 flush)。这通常是好事,因为它效率更高。但是,如果您想立即检查乐观锁定失败、执行对象验证或通常想获得即时反馈,可以通过调用entity.flush()
来强制执行 flush 操作,甚至可以使用entity.persistAndFlush()
将其合并为一个方法调用。这将允许您捕获 Hibernate Reactive 将这些更改发送到数据库时可能发生的任何PersistenceException
。请记住,这效率较低,因此不要滥用它。而且您的事务仍然需要提交。
以下是 flush 方法用例的示例,以在PersistenceException
发生时允许执行特定操作
@WithTransaction
public Uni<Void> create(Person person){
// Here we use the persistAndFlush() shorthand method on a Panache repository to persist to database then flush the changes.
return person.persistAndFlush()
.onFailure(PersistenceException.class)
.recoverWithItem(() -> {
LOG.error("Unable to create the parameter", pe);
//in case of error, I save it to disk
diskPersister.save(person);
return null;
});
}
@WithTransaction
注解也适用于测试。这意味着在测试期间所做的更改将传播到数据库。如果您希望在测试结束时回滚所做的任何更改,您可以使用io.quarkus.test.TestReactiveTransaction
注解。这将在一个事务中运行测试方法,但在测试方法完成后将其回滚以撤销任何数据库更改。
锁定管理
Panache 使用findById(Object, LockModeType)
或find().withLock(LockModeType)
为您的实体/存储库提供对数据库锁定的直接支持。
以下示例适用于活动记录模式,但也可用于存储库。
首先:使用 findById() 进行锁定。
public class PersonEndpoint {
@GET
public Uni<Person> findByIdForUpdate(Long id){
return Panache.withTransaction(() -> {
return Person.<Person>findById(id, LockModeType.PESSIMISTIC_WRITE)
.invoke(person -> {
//do something useful, the lock will be released when the transaction ends.
});
});
}
}
其次:在 find() 中进行锁定。
public class PersonEndpoint {
@GET
public Uni<Person> findByNameForUpdate(String name){
return Panache.withTransaction(() -> {
return Person.<Person>find("name", name).withLock(LockModeType.PESSIMISTIC_WRITE).firstResult()
.invoke(person -> {
//do something useful, the lock will be released when the transaction ends.
});
});
}
}
请注意,锁定会在事务结束时释放,因此调用锁定查询的方法必须在事务内调用。
自定义 ID
ID 经常是一个棘手的问题,并非每个人都愿意让框架来处理它们,我们再次为您提供了解决方案。
您可以通过继承PanacheEntityBase
而不是PanacheEntity
来指定自己的 ID 策略。然后,您只需将任何您想要的 ID 声明为公共字段
@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 类型指定为额外的类型参数
@ApplicationScoped
public class PersonRepository implements PanacheRepositoryBase<Person,Integer> {
//...
}
测试
在@QuarkusTest
中测试响应式 Panache 实体比测试常规 Panache 实体要复杂一些,因为 API 是异步的,并且所有操作都需要在 Vert.x 事件循环上运行。
quarkus-test-vertx
依赖项提供了@io.quarkus.test.vertx.RunOnVertxContext
注解和io.quarkus.test.vertx.UniAsserter
类,它们正是为此目的而设计的。使用方法在Hibernate Reactive指南中有描述。
此外,quarkus-test-hibernate-reactive-panache
依赖项提供了io.quarkus.test.hibernate.reactive.panache.TransactionalUniAsserter
,它可以作为用@RunOnVertxContext
注解的测试方法的参数注入。TransactionalUniAsserter
是一个io.quarkus.test.vertx.UniAsserterInterceptor
,它将每个断言方法包装在单独的响应式事务中。
TransactionalUniAsserter
示例import io.quarkus.test.hibernate.reactive.panache.TransactionalUniAsserter;
@QuarkusTest
public class SomeTest {
@Test
@RunOnVertxContext
public void testEntity(TransactionalUniAsserter asserter) {
asserter.execute(() -> new MyEntity().persist()); (1)
asserter.assertEquals(() -> MyEntity.count(), 1l); (2)
asserter.execute(() -> MyEntity.deleteAll()); (3)
}
}
1 | 第一个响应式事务用于持久化实体。 |
2 | 第二个响应式事务用于计数实体。 |
3 | 第三个响应式事务用于删除所有实体。 |
当然,您也可以定义一个自定义的UniAsserterInterceptor
来包装注入的UniAsserter
并自定义行为。
模拟
使用活动记录模式
如果您使用活动记录模式,则不能直接使用 Mockito,因为它不支持模拟静态方法,但您可以使用quarkus-panache-mock
模块,该模块允许您使用 Mockito 来模拟所有提供的静态方法,包括您自己的。
将此依赖项添加到您的构建文件中
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-mock</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-panache-mock")
给定此简单实体
@Entity
public class Person extends PanacheEntity {
public String name;
public static Uni<List<Person>> findOrdered() {
return find("ORDER BY name").list();
}
}
您可以像这样编写您的模拟测试
import io.quarkus.test.vertx.UniAsserter;
import io.quarkus.test.vertx.RunOnVertxContext;
@QuarkusTest
public class PanacheFunctionalityTest {
@RunOnVertxContext (1)
@Test
public void testPanacheMocking(UniAsserter asserter) { (2)
asserter.execute(() -> PanacheMock.mock(Person.class));
// Mocked classes always return a default value
asserter.assertEquals(() -> Person.count(), 0l);
// Now let's specify the return value
asserter.execute(() -> Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(23l)));
asserter.assertEquals(() -> Person.count(), 23l);
// Now let's change the return value
asserter.execute(() -> Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(42l)));
asserter.assertEquals(() -> Person.count(), 42l);
// Now let's call the original method
asserter.execute(() -> Mockito.when(Person.count()).thenCallRealMethod());
asserter.assertEquals(() -> Person.count(), 0l);
// Check that we called it 4 times
asserter.execute(() -> {
PanacheMock.verify(Person.class, Mockito.times(4)).count(); (3)
});
// Mock only with specific parameters
asserter.execute(() -> {
Person p = new Person();
Mockito.when(Person.findById(12l)).thenReturn(Uni.createFrom().item(p));
asserter.putData(key, p);
});
asserter.assertThat(() -> Person.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key)));
asserter.assertNull(() -> Person.findById(42l));
// Mock throwing
asserter.execute(() -> Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException()));
asserter.assertFailedWith(() -> {
try {
return Person.findById(12l);
} catch (Exception e) {
return Uni.createFrom().failure(e);
}
}, t -> assertEquals(WebApplicationException.class, t.getClass()));
// We can even mock your custom methods
asserter.execute(() -> Mockito.when(Person.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList())));
asserter.assertThat(() -> Person.findOrdered(), list -> list.isEmpty());
asserter.execute(() -> {
PanacheMock.verify(Person.class).findOrdered();
PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any());
PanacheMock.verifyNoMoreInteractions(Person.class);
});
// IMPORTANT: We need to execute the asserter within a reactive session
asserter.surroundWith(u -> Panache.withSession(() -> u));
}
}
1 | 确保测试方法在 Vert.x 事件循环上运行。 |
2 | 注入的UniAsserter 参数用于进行断言。 |
3 | 请确保在PanacheMock 上调用您的verify 和do* 方法,而不是在Mockito 上调用,否则您将不知道要传递哪个模拟对象。 |
使用存储库模式
如果您使用的是存储库模式,您可以直接使用 Mockito,使用 quarkus-junit5-mockito
模块,这使得模拟 bean 变得更加容易
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-junit5-mockito")
给定此简单实体
@Entity
public class Person {
@Id
@GeneratedValue
public Long id;
public String name;
}
和此存储库
@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {
public Uni<List<Person>> findOrdered() {
return find("ORDER BY name").list();
}
}
您可以像这样编写您的模拟测试
import io.quarkus.test.vertx.UniAsserter;
import io.quarkus.test.vertx.RunOnVertxContext;
@QuarkusTest
public class PanacheFunctionalityTest {
@InjectMock
PersonRepository personRepository;
@RunOnVertxContext (1)
@Test
public void testPanacheRepositoryMocking(UniAsserter asserter) { (2)
// Mocked classes always return a default value
asserter.assertEquals(() -> mockablePersonRepository.count(), 0l);
// Now let's specify the return value
asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(23l)));
asserter.assertEquals(() -> mockablePersonRepository.count(), 23l);
// Now let's change the return value
asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(42l)));
asserter.assertEquals(() -> mockablePersonRepository.count(), 42l);
// Now let's call the original method
asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenCallRealMethod());
asserter.assertEquals(() -> mockablePersonRepository.count(), 0l);
// Check that we called it 4 times
asserter.execute(() -> {
Mockito.verify(mockablePersonRepository, Mockito.times(4)).count();
});
// Mock only with specific parameters
asserter.execute(() -> {
Person p = new Person();
Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(Uni.createFrom().item(p));
asserter.putData(key, p);
});
asserter.assertThat(() -> mockablePersonRepository.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key)));
asserter.assertNull(() -> mockablePersonRepository.findById(42l));
// Mock throwing
asserter.execute(() -> Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException()));
asserter.assertFailedWith(() -> {
try {
return mockablePersonRepository.findById(12l);
} catch (Exception e) {
return Uni.createFrom().failure(e);
}
}, t -> assertEquals(WebApplicationException.class, t.getClass()));
// We can even mock your custom methods
asserter.execute(() -> Mockito.when(mockablePersonRepository.findOrdered())
.thenReturn(Uni.createFrom().item(Collections.emptyList())));
asserter.assertThat(() -> mockablePersonRepository.findOrdered(), list -> list.isEmpty());
asserter.execute(() -> {
Mockito.verify(mockablePersonRepository).findOrdered();
Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any());
Mockito.verify(mockablePersonRepository).persist(Mockito.<Person> any());
Mockito.verifyNoMoreInteractions(mockablePersonRepository);
});
// IMPORTANT: We need to execute the asserter within a reactive session
asserter.surroundWith(u -> Panache.withSession(() -> u));
}
}
1 | 确保测试方法在 Vert.x 事件循环上运行。 |
2 | 注入的UniAsserter 参数用于进行断言。 |
我们如何以及为何简化 Hibernate Reactive 映射
在编写 Hibernate Reactive 实体时,用户已经习惯于不情愿地处理一些恼人的事情,例如
-
复制 ID 逻辑:大多数实体都需要 ID,大多数人不在乎它如何设置,因为它与您的模型无关。
-
笨拙的 getter 和 setter:由于 Java 在语言级别缺乏对属性的支持,我们必须创建字段,然后为这些字段生成 getter 和 setter,即使它们实际上除了读取/写入字段之外什么也不做。
-
传统的 EE 模式建议将实体定义(模型)与其操作(DAO、存储库)分开,但这需要将状态与其操作进行不自然的分割,即使我们在面向对象架构中从不对普通对象这样做,其中状态和方法在同一个类中。此外,这需要每个实体有两个类,并在需要执行实体操作的地方注入 DAO 或存储库,这会破坏您的编辑流程,并需要您离开正在编写的代码来设置注入点,然后再回来使用它。
-
Hibernate 查询功能非常强大,但对于常见操作来说过于冗长,即使您不需要所有部分也需要您编写查询。
-
Hibernate 非常通用,但不能轻松完成构成我们模型使用 90% 的琐碎操作。
使用 Panache,我们采用了自以为是的策略来解决所有这些问题
-
让您的实体继承
PanacheEntity
:它有一个自动生成的 ID 字段。如果您需要自定义 ID 策略,可以改用PanacheEntityBase
并自行处理 ID。 -
使用公共字段。摆脱笨拙的 getter 和 setter。在后台,我们将生成所有缺失的 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 Reactive 从未如此精简和整洁。