The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

PanacheでシンプルになったHibernate Reactive

Hibernate Reactive is the only reactive 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:

@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) クエリの断片を取り、残りの部分をコンテキスト化します。これにより、非常に簡潔でありながら読みやすいコードになっています。
上記で説明したものは、基本的には アクティブレコードパターン であり、エンティティーパターンと呼ばれることもあります。Hibernate with Panache は、 PanacheRepository を通じて、より古典的な リポジトリパターン を使用することもできます。

ソリューション

次のセクションで紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

Gitレポジトリをクローンするか git clone https://github.com/quarkusio/quarkus-quickstarts.gitアーカイブ をダウンロードします。

The solution is located in the hibernate-reactive-panache-quickstart directory.

プロジェクトがすでに他のアノテーションプロセッサーを使用するように設定されている場合、追加でPanacheアノテーションプロセッサーを追加する必要があります:

pom.xml
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${compiler-plugin.version}</version>
    <configuration>
        <parameters>${maven.compiler.parameters}</parameters>
        <annotationProcessorPaths>
            <!-- Your existing annotation processor(s)... -->
            <path>
                <groupId>io.quarkus</groupId>
                <artifactId>quarkus-panache-common</artifactId>
                <version>${quarkus.platform.version}</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>
build.gradle
annotationProcessor("io.quarkus:quarkus-panache-common")

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)

pom.xml で、以下の依存関係を追加します:

  • the Hibernate Reactive with Panache extension

  • your reactive driver extension (quarkus-reactive-pg-client, quarkus-reactive-mysql-client, quarkus-reactive-db2-client, …​)

例えば

pom.xml
<!-- 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>
build.gradle
// 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

解決策1:アクティブレコードパターンを使用する

エンティティの定義

Panache エンティティーを定義するには、 PanacheEntity を拡張して @Entity とアノテーションを付け、列をパブリック フィールドとして追加します。

@Entity
public class Person extends PanacheEntity {
    public String name;
    public LocalDate birth;
    public Status status;
}

You can put all your JPA 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.

最も便利な操作

エンティティーを記述したら、ここでは実行できる最も一般的な操作を紹介します。

// 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's 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

エンティティの定義

リポジトリパターンを使用する場合、エンティティーを通常のJPAエンティティーとして定義することができます。

@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.

リポジトリの定義

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

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's 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);
残りのドキュメントでは、アクティブレコードパターンに基づく使用法のみを示していますが、リポジトリパターンでも実行できることを覚えておいてください。リポジトリパターンの例は簡潔にするために省略しています。

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 UnsupportedOperationException; you can switch back to paging using page(Page) or page(int, int).

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 to from EntityName order by …​

  • <singleColumnName> (and single parameter) which will expand to from EntityName where <singleColumnName> = ?

  • <query> will expand to from EntityName where <query>

If your update query does not start with update, we support the following additional forms:

  • from EntityName …​ which will expand to update from EntityName …​

  • set? <singleColumnName> (and single parameter) which will expand to update from EntityName set <singleColumnName> = ?

  • set? <update-query> will expand to update 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 to delete from EntityName …​

  • <singleColumnName> (and single parameter) which will expand to delete from EntityName where <singleColumnName> = ?

  • <query> will expand to delete 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 JPA entity classes (being the Panache entity class, or the repository parameterized type), or on one of its super classes.

クエリパラメーター

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 @RegisterForReflection アノテーションは、ネイティブコンパイル時にクラスとそのメンバーを保持するようQuarkusに指示します。 @RegisterForReflection アノテーションの詳細については、 ネイティブアプリケーションのヒントのページを参照してください。
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 project(Class) method uses the constructor’s parameter names to build the select clause of the query, so the compiler must be configured to store parameter names inside the compiled class. This is enabled by default if you are using the Quarkus Maven archetype. If you are not using it, add the property <maven.compiler.parameters>true</maven.compiler.parameters> to your pom.xml.

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;
    @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.

複数の永続性ユニット

Hibernate Reactive in Quarkus currently does not support multiple persistence units.

トランザクション

Make sure to wrap methods modifying your database (e.g. entity.persist()) within a transaction. Marking a CDI bean method @ReactiveTransactional will do that for you and make that method a transaction boundary. Alternatively, you can use Panache.withTransaction() for the same effect. We recommend doing so at your application entry point boundaries like your REST endpoint controllers.

You cannot use @Transactional with Hibernate Reactive for your transactions: you must use @ReactiveTransactional, and your annotated method must return a Uni to be non-blocking. Otherwise, it needs be called from a non-VertxThread thread and will become blocking.

JPA batches changes you make to your entities and sends changes (it’s called flush) at the end of the transaction or before a query. This is usually a good thing as it’s 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 JPA 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:

@ReactiveTransactional
public Uni<Void> create(Person person){
    //Here I 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 @ReactiveTransactional 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> {
    //...
}

モック

アクティブレコードパターンの使用

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:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-panache-mock</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
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:

@QuarkusTest
public class PanacheFunctionalityTest {

    @Test
    public void testPanacheMocking() {
        PanacheMock.mock(Person.class);

        // Mocked classes always return a default value
        Assertions.assertEquals(0, Person.count().await().indefinitely());

        // Now let's specify the return value
        Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(23l));
        Assertions.assertEquals(23, Person.count().await().indefinitely());

        // Now let's change the return value
        Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(42l));
        Assertions.assertEquals(42, Person.count().await().indefinitely());

        // Now let's call the original method
        Mockito.when(Person.count()).thenCallRealMethod();
        Assertions.assertEquals(0, Person.count().await().indefinitely());

        // Check that we called it 4 times
        PanacheMock.verify(Person.class, Mockito.times(4)).count();(1)

        // Mock only with specific parameters
        Person p = new Person();
        Mockito.when(Person.findById(12l)).thenReturn(Uni.createFrom().item(p));
        Assertions.assertSame(p, Person.findById(12l).await().indefinitely());
        Assertions.assertNull(Person.findById(42l).await().indefinitely());

        // Mock throwing
        Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException());
        try {
            Person.findById(12l);
            Assertions.fail();
        } catch (WebApplicationException x) {
        }

        // We can even mock your custom methods
        Mockito.when(Person.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList()));
        Assertions.assertTrue(Person.findOrdered().await().indefinitely().isEmpty());

        PanacheMock.verify(Person.class).findOrdered();
        PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any());
        PanacheMock.verifyNoMoreInteractions(Person.class);
    }
}
1 Be sure to call your verify and do* methods on PanacheMock rather than Mockito, otherwise you won’t know what mock object to pass.

Mocking Mutiny.Session and entity instance methods

If you need to mock entity instance methods, such as persist() you can do it by mocking the Hibernate Reactive Mutiny.Session object:

@QuarkusTest
public class PanacheMockingTest {

    @InjectMock
    Mutiny.Session session;

    @Test
    public void testPanacheSessionMocking() {
        Person p = new Person();
        // mocked via Mutiny.Session mocking
        p.persist().await().indefinitely();
        Assertions.assertNull(p.id);

        Mockito.verify(session, Mockito.times(1)).persist(Mockito.any());
    }
}

リポジトリパターンの使用

If you are using the repository pattern you can use Mockito directly, using the quarkus-junit5-mockito module, which makes mocking beans much easier:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5-mockito</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
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:

@QuarkusTest
public class PanacheFunctionalityTest {
    @InjectMock
    PersonRepository personRepository;

    @Test
    public void testPanacheRepositoryMocking() throws Throwable {
        // Mocked classes always return a default value
        Assertions.assertEquals(0, mockablePersonRepository.count().await().indefinitely());

        // Now let's specify the return value
        Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(23l));
        Assertions.assertEquals(23, mockablePersonRepository.count().await().indefinitely());

        // Now let's change the return value
        Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(42l));
        Assertions.assertEquals(42, mockablePersonRepository.count().await().indefinitely());

        // Now let's call the original method
        Mockito.when(mockablePersonRepository.count()).thenCallRealMethod();
        Assertions.assertEquals(0, mockablePersonRepository.count().await().indefinitely());

        // Check that we called it 4 times
        Mockito.verify(mockablePersonRepository, Mockito.times(4)).count();

        // Mock only with specific parameters
        Person p = new Person();
        Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(Uni.createFrom().item(p));
        Assertions.assertSame(p, mockablePersonRepository.findById(12l).await().indefinitely());
        Assertions.assertNull(mockablePersonRepository.findById(42l).await().indefinitely());

        // Mock throwing
        Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException());
        try {
            mockablePersonRepository.findById(12l);
            Assertions.fail();
        } catch (WebApplicationException x) {
        }

        // We can even mock your custom methods
        Mockito.when(mockablePersonRepository.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList()));
        Assertions.assertTrue(mockablePersonRepository.findOrdered().await().indefinitely().isEmpty());

        Mockito.verify(mockablePersonRepository).findOrdered();
        Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any());
        Mockito.verify(mockablePersonRepository).persist(Mockito.<Person> any());
        Mockito.verifyNoMoreInteractions(mockablePersonRepository);
    }
}

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:

  • IDロジックの重複:ほとんどのエンティティにはIDが必要ですが、モデルとはあまり関係がないため、ほとんどの人はIDの設定方法を気にしません。

  • 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のクエリは非常に強力ですが、一般的な操作には冗長すぎるため、すべての部分が必要ない場合でもクエリを書く必要があります。

  • Hibernateは非常に汎用性が高いのですが、モデルの使用量の9割を占めるような些細な操作をしても些細にはなりません。

Panacheでは、これらの問題に対して、定見に基づいたアプローチをとりました。

  • Make your entities extend PanacheEntity: it has an ID field that is auto-generated. If you require a custom ID strategy, you can extend PanacheEntityBase 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 typing Person. 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") or Person.find("name = ?1 and status = ?2", "stef", Status.Alive) or even better Person.find("name", "stef").

That’s all there is to it: with Panache, Hibernate Reactive has never looked so trim and neat.

外部プロジェクトや jar でエンティティーを定義する

Hibernate Reactive with Panache relies on compile-time bytecode enhancements to your entities.

この機能は、マーカーファイル META-INF/panache-archive.marker の存在によって Panache エンティティー の存在するアーカイブ(および Panache エンティティーの消費者) を識別しようとします 。Panache にはアノテーション プロセッサーが含まれており、 (間接的であっても) Panache に依存しているアーカイヴでこのファイルを自動的に作成します。アノテーションプロセッサーを無効にしている場合は、場合によってはこのファイルを手動で作成する必要があるかもしれません。

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>