Simplified Hibernate Reactive with Panache
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.
Hibernate Reactive is not a replacement for Hibernate ORM or the future of Hibernate ORM. It is a different stack tailored for reactive use cases where you need high-concurrency. Furthermore, using RESTEasy Reactive, our default REST layer, does not require the use of Hibernate Reactive. It is perfectly valid to use RESTEasy Reactive with Hibernate ORM, and if you do not need high-concurrency, or are not accustomed to the reactive paradigm, it is recommended to use Hibernate ORM. |
First: an example
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");
}
}
You have noticed how much more compact and readable the code is? Does this look interesting? Read on!
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 .
|
完整源码
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
Clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git
, or download
an archive.
The solution is located in the hibernate-reactive-panache-quickstart
directory.
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
|
Setting up and configuring Hibernate Reactive with Panache
To get started:
-
add your settings in
application.properties
-
annotate your entities with
@Entity
-
make your entities extend
PanacheEntity
(optional if you are using the repository pattern)
Follow the Hibernate set-up guide for all configuration.
In your pom.xml
, add the following dependencies:
-
the Hibernate Reactive with Panache extension
-
your reactive driver extension (
quarkus-reactive-pg-client
,quarkus-reactive-mysql-client
,quarkus-reactive-db2-client
, …)
For instance:
<!-- 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")
Then add the relevant configuration properties in 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
Solution 1: using the active record pattern
Defining your entity
To define a Panache entity, simply extend PanacheEntity
, annotate it with
@Entity
and add your columns as public fields:
@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();
}
}
And thanks to our field access rewrite, when your users read person.name
they will actually call your getName()
accessor, and similarly for field
writes and the setter. This allows for proper encapsulation at runtime as
all fields calls will be replaced by the corresponding getter/setter calls.
Most useful operations
Once you have written your entity, here are the most common operations you will be able to perform:
// 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);
Adding entity methods
Add custom queries on your entities inside the entities themselves. That way, you and your co-workers can find them easily, and queries are co-located with the object they operate on. Adding them as static methods in your entity class is the Panache Active Record way.
@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");
}
}
Solution 2: using the repository pattern
Defining your entity
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;
}
}
If you don’t want to bother defining getters/setters for your entities, you
can make them extend PanacheEntityBase and Quarkus will generate them for
you. You can even extend PanacheEntity and take advantage of the default
ID it provides.
|
Defining your repository
When using Repositories, you get the exact same convenient methods as with
the active record pattern, injected in your Repository, by making them
implements 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");
}
}
All the operations that are defined on PanacheEntityBase
are available on
your repository, so using it is exactly the same as using the active record
pattern, except you need to inject it:
@Inject
PersonRepository personRepository;
@GET
public Uni<Long> count(){
return personRepository.count();
}
Most useful operations
Once you have written your repository, here are the most common operations you will be able to perform:
// 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);
The rest of the documentation show usages based on the active record pattern only, but keep in mind that they can be performed with the repository pattern as well. The repository pattern examples have been omitted for brevity. |
Advanced Query
Paging
You should only use the list
methods if your table contains small enough
data sets. For larger data sets you can use the find
method equivalents,
which return a PanacheQuery
on which you can do paging:
// 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();
The PanacheQuery
type has many other methods to deal with paging and
returning streams.
Using a range instead of pages
PanacheQuery
also allows range-based queries.
// 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();
You cannot mix ranges and pages: if you use a range, all methods that depend
on having a current page will throw an |
Sorting
All methods accepting a query string also accept the following simplified query form:
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.
Simplified queries
Normally, HQL queries are of this form: from EntityName [where …] [order
by …]
, with optional elements at the end.
If your select query does not start with from
, we support the following
additional forms:
-
order by …
which will expand tofrom EntityName order by …
-
<singleColumnName>
(and single parameter) which will expand tofrom EntityName where <singleColumnName> = ?
-
<query>
will expand tofrom EntityName where <query>
If your update query does not start with update
, we support the following
additional forms:
-
from EntityName …
which will expand toupdate from EntityName …
-
set? <singleColumnName>
(and single parameter) which will expand toupdate from EntityName set <singleColumnName> = ?
-
set? <update-query>
will expand toupdate from EntityName set <update-query>
If your delete query does not start with delete
, we support the following
additional forms:
-
from EntityName …
which will expand todelete from EntityName …
-
<singleColumnName>
(and single parameter) which will expand todelete from EntityName where <singleColumnName> = ?
-
<query>
will expand todelete from EntityName where <query>
You can also write your queries in plain HQL: |
Order.find("select distinct o from Order o left join fetch o.lineItems");
Order.update("update from Person set name = 'Mortal' where status = ?", Status.Alive);
Named queries
You can reference a named query instead of a (simplified) HQL query by prefixing its name with the '#' character. You can also use named queries for count, update and delete queries.
@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. |
Query parameters
You can pass query parameters by index (1-based) as shown below:
Person.find("name = ?1 and status = ?2", "stef", Status.Alive);
Or by name using a Map
:
Map<String, Object> params = new HashMap<>();
params.put("name", "stef");
params.put("status", Status.Alive);
Person.find("name = :name and status = :status", params);
Or using the convenience class Parameters
either as is or to build a
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));
Every query operation accepts passing parameters by index (Object…
), or
by name (Map<String,Object>
or Parameters
).
Query projection
Query projection can be done with the project(Class)
method on the
PanacheQuery
object that is returned by the find()
methods.
You can use it to restrict which fields will be returned by the database.
Hibernate will use DTO projection and generate a SELECT clause with the attributes from the projection class. This is also called dynamic instantiation or constructor expression, more info can be found on the Hibernate guide: hql select clause
The projection class needs to be a valid Java Bean and have a constructor that contains all its attributes, this constructor will be used to instantiate the projection DTO instead of using the entity class. This must be the only constructor of the class.
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 | The @RegisterForReflection annotation instructs Quarkus to keep the class
and its members during the native compilation. More details about the
@RegisterForReflection annotation can be found on the
native
application tips page. |
2 | We use public fields here, but you can use private fields and getters/setters if you prefer. |
3 | This constructor will be used by Hibernate, and it must have a matching constructor with all the class attributes as parameters. |
The implementation of the |
If in the DTO projection object you have a field from a referenced entity,
you can use the @ProjectedFieldName
annotation to provide the path for the
SELECT statement.
@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 | The ownerName DTO constructor’s parameter will be loaded from the
owner.name HQL property. |
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:
|
Multiple Persistence Units
Hibernate Reactive in Quarkus currently does not support multiple persistence units.
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.
Here is an example of the usage of the flush method to allow making a
specific action in case of 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.
Lock management
Panache provides direct support for database locking with your
entity/repository, using findById(Object, LockModeType)
or
find().withLock(LockModeType)
.
The following examples are for the active record pattern, but the same can be used with repositories.
First: Locking using 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.
});
});
}
}
Second: Locking in a 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.
});
});
}
}
Be careful that locks are released when the transaction ends, so the method that invokes the lock query must be called within a transaction.
Custom IDs
IDs are often a touchy subject, and not everyone’s up for letting them handled by the framework, once again we have you covered.
You can specify your own ID strategy by extending PanacheEntityBase
instead of PanacheEntity
. Then you just declare whatever ID you want as a
public field:
@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;
//...
}
If you’re using repositories, then you will want to extend
PanacheRepositoryBase
instead of PanacheRepository
and specify your ID
type as an extra type parameter:
@ApplicationScoped
public class PersonRepository implements PanacheRepositoryBase<Person,Integer> {
//...
}
Testing
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.
Moreover, the quarkus-test-hibernate-reactive-panache
dependency provides
the io.quarkus.test.hibernate.reactive.panache.TransactionalUniAsserter
that can be injected as a method parameter of a test method annotated with
@RunOnVertxContext
. The TransactionalUniAsserter
is a
io.quarkus.test.vertx.UniAsserterInterceptor
that wraps each assert method
within a separate reactive transaction.
TransactionalUniAsserter
Exampleimport 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 | The first reactive transaction is used to persist the entity. |
2 | The second reactive transaction is used to count the entities. |
3 | The third reactive transaction is used to delete all entities. |
Of course, you can also define a custom UniAsserterInterceptor
to wrap the
injected UniAsserter
and customize the behavior.
Mocking
Using the active record pattern
If you are using the active record pattern you cannot use Mockito directly
as it does not support mocking static methods, but you can use the
quarkus-panache-mock
module which allows you to use Mockito to mock all
provided static methods, including your own.
Add this dependency to your build file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-mock</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-panache-mock")
Given this simple entity:
@Entity
public class Person extends PanacheEntity {
public String name;
public static Uni<List<Person>> findOrdered() {
return find("ORDER BY name").list();
}
}
You can write your mocking test like this:
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 argument is used to make assertions. |
3 | Be sure to call your verify and do* methods on PanacheMock rather than
Mockito , otherwise you won’t know what mock object to pass. |
Using the repository pattern
If you are using the repository pattern you can use Mockito directly, using
the quarkus-junit5-mockito
module, which makes mocking beans much easier:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-junit5-mockito")
Given this simple entity:
@Entity
public class Person {
@Id
@GeneratedValue
public Long id;
public String name;
}
And this repository:
@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {
public Uni<List<Person>> findOrdered() {
return find("ORDER BY name").list();
}
}
You can write your mocking test like this:
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. |
How and why we simplify Hibernate Reactive mappings
When it comes to writing Hibernate Reactive entities, there are a number of annoying things that users have grown used to reluctantly deal with, such as:
-
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.
-
Dumb getters and setters: since Java lacks support for properties in the language, we have to create fields, then generate getters and setters for those fields, even if they don’t actually do anything more than read/write the fields.
-
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 queries are super powerful, but overly verbose for common operations, requiring you to write queries even when you don’t need all the parts.
-
Hibernate is very general-purpose, but does not make it trivial to do trivial operations that make up 90% of our model usage.
With Panache, we took an opinionated approach to tackle all these problems:
-
Make your entities extend
PanacheEntity
: it has an ID field that is auto-generated. If you require a custom ID strategy, you can extendPanacheEntityBase
instead and handle the ID yourself. -
Use public fields. Get rid of dumb getter and setters. Under the hood, we will generate all getters and setters that are missing, and rewrite every access to these fields to use the accessor methods. This way you can still write useful accessors when you need them, which will be used even though your entity users still use field accesses.
-
With the active record pattern: put all your entity logic in static methods in your entity class and don’t create DAOs. Your entity superclass comes with lots of super useful static methods, and you can add your own in your entity class. Users can just start using your entity
Person
by typingPerson.
and getting completion for all the operations in a single place. -
Don’t write parts of the query that you don’t need: write
Person.find("order by name")
orPerson.find("name = ?1 and status = ?2", "stef", Status.Alive)
or even betterPerson.find("name", "stef")
.
That’s all there is to it: with Panache, Hibernate Reactive has never looked so trim and neat.
Defining entities in external projects or jars
Hibernate Reactive with Panache relies on compile-time bytecode enhancements to your entities.
It attempts to identify archives with Panache entities (and consumers of
Panache entities) by the presence of the marker file
META-INF/panache-archive.marker
. Panache includes an annotation processor
that will automatically create this file in archives that depend on Panache
(even indirectly). If you have disabled annotation processors you may need
to create this file manually in some cases.
If you include the jpa-modelgen annotation processor this will exclude the
Panache annotation processor by default. If you do this you should either
create the marker file yourself, or add the quarkus-panache-common as
well, as shown below:
|
<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>