使用Panache简化Hibernate Reactive
Hibernate Reactive is the only reactive Jakarta Persistence (formerly known as JPA) implementation and offers you the full breadth of an Object Relational Mapper allowing you to access your database over reactive drivers. It makes complex mappings possible, but it does not make simple and common mappings trivial. Hibernate Reactive with Panache focuses on making your entities trivial and fun to write in Quarkus.
首先看一个例子
What we’re doing in Panache allows you to write your Hibernate Reactive entities like this:
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");
}
}
你有注意到代码的紧凑性和可读性大大提高了吗?看起来很有趣吧?请继续阅读!
The list() method might be surprising at first. It takes fragments of HQL (JP-QL) queries and contextualizes the rest. That makes for very concise but yet readable code.
|
What was described above is essentially the active record pattern, sometimes just called the entity pattern. Hibernate with Panache also allows for the use of the more classical repository pattern via PanacheRepository .
|
解决方案
我们建议你按照下面几节的说明,一步一步地创建应用程序。当然,你也可以直接使用已完成的样例工程。
克隆 Git 仓库可使用命令: git clone https://github.com/quarkusio/quarkus-quickstarts.git
,或者下载 压缩包 。
该解决方案位于 hibernate-reactive-panache-quickstart
目录 中。
If your project is already configured to use other annotation processors, you will need to additionally add the Panache annotation processor: pom.xml
build.gradle
|
在Hibernate Reactive中配置Panache
按以下步骤开始:
-
在
application.properties
中添加你的设置。 -
给实体类增加
@Entity
注解 -
实体类改为继承
PanacheEntity
类(使用Repository模式时为可选操作)。
按照 Hibernate 设置指南 进行其他配置。
在你的 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://localhost:5432/mydatabase
# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation = drop-and-create
解决方案1:使用Active Record模式
定义实体类
要定义一个Panache实体类,只需继承 PanacheEntity
,增加 @Entity
注解,并将数据库列作为公共字段添加到实体类:
@Entity
public class Person extends PanacheEntity {
public String name;
public LocalDate birth;
public Status status;
}
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:
@Entity
public class Person extends PanacheEntity {
public String name;
public LocalDate birth;
public Status status;
// return name as uppercase in the model
public String getName(){
return name.toUpperCase();
}
// store all names in lowercase in the DB
public void setName(String name){
this.name = name.toLowerCase();
}
}
由于重写了字段访问器,当用户读取 person.name
时,他们实际上会调用你的 getName()
访问器方法;类似的还有字段写入时调用设置器方法。这样的机制允许在运行时进行适当的封装,因为所有的字段调用都会替代为相应的 getter/setter 方法调用。
常用操作
写好了实体类之后,你可以执行以下一些最常见的操作:
// creating a person
Person person = new Person();
person.name = "Stef";
person.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
person.status = Status.Alive;
// persist it
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:使用Repository模式
定义实体类
When using the repository pattern, you can define your entities as regular Jakarta Persistence entities.
@Entity
public class Person {
@Id @GeneratedValue private Long id;
private String name;
private LocalDate birth;
private Status status;
public Long getId(){
return id;
}
public void setId(Long id){
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalDate getBirth() {
return birth;
}
public void setBirth(LocalDate birth) {
this.birth = birth;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
}
如果你不想自己定义实体的getter和setter方法,可以让实体类继承 PanacheEntityBase ,Quarkus将自动生成getter和setter方法。你也可以继承 PanacheEntity ,相比 PanacheEntityBase ,其优势是它还提供默认的ID字段。
|
定义Repository
使用 Repository 模式时,通过实现 PanacheRepository
接口,你可以使用与Active Record模式下完全相同的便捷方法:
@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {
// put your custom logic here as instance methods
public 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
中定义的所有方法都可以在你的Repository类上使用,所以它使用起来与Active Record模式完全一样,只是你需要注入Repository类的实例:
@Inject
PersonRepository personRepository;
@GET
public Uni<Long> count(){
return personRepository.count();
}
常用操作
写好了Repository类之后,你可以执行以下一些最常见的操作:
// creating a person
Person person = new Person();
person.setName("Stef");
person.setBirth(LocalDate.of(1910, Month.FEBRUARY, 1));
person.setStatus(Status.Alive);
// persist it
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);
后续的文档只展示了基于Active Record模式的用法,但请记住,这些用法也可以用于Repository模式。为了简洁起见,省略了Repository模式的例子。 |
高级查询
分页
如果你的表数据量很小,你应该只用到 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");
But these methods also accept an optional Sort
parameter, which allows you to abstract your sorting:
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));
The Sort
class has plenty of methods for adding columns and specifying sort direction or the null precedence.
简化查询
通常情况下,HQL查询语句是这种形式: from EntityName [where …] [order by …]
,结尾处有可选元素。
如果你的查询语句不是以 from
开始,我们还支持以下的形式:
-
order by …
语句会被扩展为:from EntityName order by …
-
<singleColumnName>
(带单个参数)语句会被扩展为:from EntityName where <singleColumnName> = ?
-
<query>
语句会被扩展为:from EntityName where <query>
如果你的更新语句不是以 update
开始,我们还支持以下的形式:
-
from EntityName …
语句会被扩展为:update from EntityName …
-
set? <singleColumnName>
(带单个参数)语句会被扩展为:update from EntityName set <singleColumnName> = ?
-
set? <update-query>
语句会被扩展为:update from EntityName set <update-query>
如果你的删除语句不是以 delete
开始,我们还支持以下的形式:
-
from EntityName …
语句会被扩展为:delete from EntityName …
-
<singleColumnName>
(带单个参数)语句会被扩展为:delete from EntityName where <singleColumnName> = ?
-
<query>
语句会被扩展为:delete from EntityName where <query>
你也可以用普通的 HQL 编写查询语句: |
Order.find("select distinct o from Order o left join fetch o.lineItems");
Order.update("update from Person set name = 'Mortal' where status = ?", Status.Alive);
命名查询
除了上述的简化HQL查询以外,你还可以定义一个命名查询,然后通过'#'字符加命名来(在HQL中)引用它。在计数、更新和删除查询中也可以使用命名查询。
@Entity
@NamedQueries({
@NamedQuery(name = "Person.getByName", query = "from Person where name = ?1"),
@NamedQuery(name = "Person.countByStatus", query = "select count(*) from Person p where p.status = :status"),
@NamedQuery(name = "Person.updateStatusById", query = "update Person p set p.status = :status where p.id = :id"),
@NamedQuery(name = "Person.deleteById", query = "delete from Person p where p.id = ?1")
})
public class Person extends PanacheEntity {
public String name;
public LocalDate birth;
public Status status;
public static 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);
}
}
Named queries can only be defined inside your Jakarta Persistence entity classes (being the Panache entity class, or the repository parameterized type), or on one of its super classes. |
查询参数
你可以通过索引(从1开始)传递查询参数,如下所示:
Person.find("name = ?1 and status = ?2", "stef", Status.Alive);
或者使用 Map
,用key做参数名:
Map<String, Object> params = new HashMap<>();
params.put("name", "stef");
params.put("status", Status.Alive);
Person.find("name = :name and status = :status", params);
或者使用 Parameters
类,也可以方便地构造一个 Map
:
// generate a Map
Person.find("name = :name and status = :status",
Parameters.with("name", "stef").and("status", Status.Alive).map());
// use it as-is
Person.find("name = :name and status = :status",
Parameters.with("name", "stef").and("status", Status.Alive));
所有查询操作都可以接收按索引( Object…
)或按名称( Map<String,Object>
或 Parameters
)传递的参数。
查询结果投影
对 find()
方法返回的 PanacheQuery
对象,可以使用 project(Class)
方法投影到指定的实体类。
你可以用投影限制数据库返回哪些字段。
Hibernate会使用 DTO投影 ,并根据投影的类的属性生成SELECT子句。这也被称为 动态实例化 或 构造器表达 ,更多信息可以在Hibernate指南中找到: HQL select子句
投影类必须是有效的Java Bean,并且拥有一个包含所有属性的构造方法,这个构造方法用于实例化投影DTO,而不是使用实体类。这个构造方法必须是唯一的构造方法。
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection (1)
public class PersonName {
public final String name; (2)
public PersonName(String name){ (3)
this.name = name;
}
}
// only 'name' will be loaded from the database
PanacheQuery<PersonName> query = Person.find("status", Status.Alive).project(PersonName.class);
1 | @RegisterForReflection 注解用于指导Quarkus在native编译过程中保留该类和其成员。关于 @RegisterForReflection 注解的更多细节可以在 native应用程序提示 页面找到。 |
2 | 在这里我们使用public的字段,你也可以使用private字段和对应的getter/setter方法。 |
3 | Hibernate会用到这个构造方法,它必须有一个匹配的构造函数,所有的类属性都是参数。 |
在 |
如果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 | DTO构造器的 ownerName 参数将从 owner.name HQL属性加载。 |
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 Reactive 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 For example, this will fail:
|
Sessions and Transactions
First of all, most of the methods of a Panache entity must be invoked within the scope of a reactive Mutiny.Session
. In some cases, the session is opened automatically on demand. For example, if a Panache entity method is invoked in a Jakarta REST resource method in an application that includes the quarkus-resteasy-reactive
extension. For other cases, there are both a declarative and a programmatic way to ensure the session is opened. You can annotate a CDI business method that returns Uni
with the @WithSession
annotation. The method will be intercepted and the returned Uni
will be triggered within a scope of a reactive session. Alternatively, you can use the Panache.withSession()
method to achieve the same effect.
Note that a Panache entity may not be used from a blocking thread. See also Getting Started With Reactive guide that explains the basics of reactive principles in Quarkus. |
Also make sure to wrap methods that modify the database or involve multiple queries (e.g. entity.persist()
) within a transaction. You can annotate a CDI business method that returns Uni
with the @WithTransaction
annotation. The method will be intercepted and the returned Uni
is triggered within a transaction boundary. Alternatively, you can use the Panache.withTransaction()
method for the same effect.
You cannot use the @Transactional annotation with Hibernate Reactive for your transactions: you must use @WithTransaction , and your annotated method must return a Uni to be non-blocking.
|
Hibernate Reactive 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 Reactive send 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
异常时执行指定操作:
@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;
});
}
The @WithTransaction
annotation will also work for testing. This means that changes done during the test will be propagated to the database. If you want any changes made to be rolled back at the end of the test you can use the io.quarkus.test.TestReactiveTransaction
annotation. This will run the test method in a transaction, but roll it back once the test method is complete to revert any database changes.
锁管理
Panache支持在实体类/Repository类中直接使用数据库的锁,可使用 findById(Object, LockModeType)
或 find().withLock(LockModeType)
方法。
下面的例子是针对Active Record模式的,但同样可以应用于Repository模式。
第一:通过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字段声明为public字段:
@Entity
public class Person extends PanacheEntityBase {
@Id
@SequenceGenerator(
name = "personSequence",
sequenceName = "person_id_seq",
allocationSize = 1,
initialValue = 4)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "personSequence")
public Integer id;
//...
}
如果你使用Repository模式,那么要继承 PanacheRepositoryBase
,而非 PanacheRepository
,并将ID字段类型作为额外的类型参数:
@ApplicationScoped
public class PersonRepository implements PanacheRepositoryBase<Person,Integer> {
//...
}
测试
Testing reactive Panache entities in a @QuarkusTest
is slightly more complicated than testing regular Panache entities due to the asynchronous nature of the APIs and the fact that all operations need to run on a Vert.x event loop.
The quarkus-test-vertx
dependency provides the @io.quarkus.test.vertx.RunOnVertxContext
annotation and the io.quarkus.test.vertx.UniAsserter
class which are intended precisely for this purpose. The usage is described in the Hibernate Reactive guide.
You can also extend the io.quarkus.test.vertx.UniAsserterInterceptor
to wrap the injected UniAsserter
and customize the behavior. For example, the interceptor can be used to execute the assert methods within a separate database transaction.
UniAsserterInterceptor
Exampleimport io.quarkus.test.vertx.UniAsserterInterceptor;
@QuarkusTest
public class SomeTest {
static class TransactionalUniAsserterInterceptor extends UniAsserterInterceptor {
public TransactionUniAsserterInterceptor(UniAsserter asserter) {
super(asserter);
}
@Override
protected <T> Supplier<Uni<T>> transformUni(Supplier<Uni<T>> uniSupplier) {
// Assert/execute methods are invoked within a database transaction
return () -> Panache.withTransaction(uniSupplier);
}
}
@Test
@RunOnVertxContext
public void testEntity(UniAsserter asserter) {
asserter = new TransactionalUniAsserterInterceptor(asserter); (1)
asserter.execute(() -> new MyEntity().persist());
asserter.assertEquals(() -> MyEntity.count(), 1l);
asserter.execute(() -> MyEntity.deleteAll());
}
}
1 | The TransactionalUniAsserterInterceptor wraps the injected UniAsserter . |
Mock模拟测试
使用Active Record模式
如果你使用了Active Record模式,那么不能直接使用Mockito,因为它不支持Mock静态方法。你可以使用 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 | Make sure the test method is run on the Vert.x event loop. |
2 | The injected UniAsserter agrument is used to make assertions. |
3 | 请确保是在 PanacheMock 上调用 verify 和 do* 方法,而不是在 Mockito 上调用,否则无法传递mock对象。 |
使用Repository模式
如果你使用Repository模式,你可以直接使用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;
}
以及这个Repository类:
@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 | Make sure the test method is run on the Vert.x event loop. |
2 | The injected UniAsserter agrument is used to make assertions. |
我们为什么简化Hibernate Reactive映射?怎么做到的?
在编写Hibernate Reactive实体类时,用户已经习惯了被迫处理许多烦人的事情,例如:
-
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.
-
愚蠢的getter和setter方法:由于Java语言中缺乏对属性的支持,我们必须创建字段,然后为这些字段生成getter和setter方法,即便这些方法实际上除了读/写字段外没有其他用处。
-
Traditional EE patterns advise to split entity definition (the model) from the operations you can do on them (DAOs, Repositories), but really that requires an unnatural 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方法。在底层,我们会生成所有缺失的getter和setter方法,并将所有对这些字段的访问重写为getter/setter方法的调用,以使用访问器方法。这样,当你需要的时候,仍可以编写 有用的 访问器,即使实体类的调用者仍然使用字段访问,实际也会使用这些访问器。
-
使用Active Record模式:把你所有的实体逻辑放在实体类的静态方法中,不要创建DAO。实体类的父类带有很多很有用的静态方法,你也可以在实体类中添加自己的静态方法。用户可以通过输入
Person.
开始使用实体Person
,并可以在同一个地方完成所有操作。 -
不要写多余的查询语句:可以写
Person.find("order by name")
或Person.find("name = ?1 and status = ?2", "stef", Status.Alive)
,甚至更好的Person.find("name", "stef")
。
以上就是它的全部内容:有了Panache,Hibernate Reactive看起来变得如此轻量和整洁。
在外部项目或jar包中定义实体
Hibernate Reactive Panache依赖于编译时对实体类的字节码增强。
Panache通过判断是否存在标记文件 META-INF/panache-archive.marker
来识别jar包是否包含 Panache 实体类(及 Panache 实体类的调用方)。Panache 包含一个注解处理器,它会自动在依赖 Panache(包括间接依赖Panache)的jar包中创建此文件。如果在某些情况下你禁用了注解处理器,可能需要手动创建此文件。
如果你的项目包含 jpa-modelgen 注解处理器,则默认情况下会排除 Panache 注解处理器。这种情况下,你应该自己创建标记文件,或者添加 quarkus-panache-common 插件,如下所示:
|
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
</annotationProcessorPath>
<annotationProcessorPath>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-common</artifactId>
<version>${quarkus.platform.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</plugin>