Getting started with Hibernate reactive on Quarkus

Hibernate Reactive in Quarkus 3.x enables end-to-end non-blocking, asynchronous database operations powered by SmallRye Mutiny and Vert.x SQL clients. This hands-on guide covers building a full reactive CRUD application with Jakarta EE standard annotations, Mutiny streams, PostgreSQL, and RESTEasy Reactive.

This tutorial will introduce you to Hibernate Reactive which enables support for non-blocking database drivers and reactive programming with Hibernate ORM in Quarkus 3.x.

Uni and Multi streams

Traditional persistence operations are designed to use blocking I/O for interaction with the database, and are therefore not appropriate for use in a fully reactive environment. Hibernate Reactive is the first ORM implementation capable of taking advantage of non-blocking database clients.

Hibernate Reactive works on top of a reactive programming environment, so you should be familiar with the concept of Reactive Streams.

Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure on the JVM.

Reactive Streams are implemented in Quarkus through the Mutiny framework, which provides asynchronous stream primitives adapting Vert.x back pressure protocols to Reactive Streams. Database operations can be wrapped by a chain of Java `CompletionStage`s or Mutiny `Uni`/`Multi` streams.

Mutiny offers two event-driven, lazy stream types:

  • A Uni emits a single event (an item or a failure). A good example is the result of executing an asynchronous database lookup or query.
  • A Multi emits multiple events (n items, 1 failure, or 1 completion). A good example is receiving a continuous stream of rows from a database cursor or messaging queue.

Using a simple pattern, you can observe item events using the following asynchronous handler:

onItem().call(item -> someAsyncAction(item))

Conversely, we can handle failure events with the recovery pattern:

Uni<String> uni1 = ...;
uni1.onFailure().recoverWithItem("my fallback value");

In the following sections, we will show how to apply these patterns in a Hibernate Reactive application that takes advantage of `Uni` streams to execute database operations in a non-blocking style.

Please note that the Hibernate Reactive API can be applied both to standard Hibernate ORM and Panache. In this tutorial, we will apply reactive streams using "classic" Hibernate ORM.

Creating a Quarkus Hibernate reactive application

To kickstart a Hibernate Reactive application on Quarkus 3.x, you need to fulfill the following requirements:

  1. Include Quarkus' quarkus-hibernate-reactive dependency instead of the default quarkus-hibernate-orm
  2. Use a non-blocking database driver compliant with Vert.x API, such as quarkus-reactive-pg-client (PostgreSQL)
  3. Include RESTEasy Reactive dependencies (standard in Quarkus 3.x)
  4. Adjust your endpoints to return io.smallrye.mutiny.Uni or io.smallrye.mutiny.Multi
  5. Use standard jakarta.* imports for persistence and REST annotations

Let's see this in action. We will bootstrap a Quarkus 3.x project as follows:

mvn io.quarkus.platform:quarkus-maven-plugin:3.18.1:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=hibernate-reactive \
    -DprojectVersion=1.0.0 \
    -DclassName="org.acme.ExampleResource"

Then, move into the folder hibernate-reactive and add the required reactive extensions:

mvn quarkus:add-extension -Dextensions="quarkus-hibernate-reactive,quarkus-reactive-pg-client,quarkus-resteasy-reactive,quarkus-resteasy-reactive-jackson"

Ok, so far so good. We will now begin coding our application components.

Our Entity class uses standard Jakarta Persistence annotations (jakarta.persistence.*):

package org.acme;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;

@Entity
@Table
@NamedQuery(name = "Customers.findAll", query = "SELECT c FROM Customer c ORDER BY c.name")
public class Customer {

    @Id
    @SequenceGenerator(name = "customersSequence", sequenceName = "known_customers_id_seq", allocationSize = 1, initialValue = 10)
    @GeneratedValue(generator = "customersSequence")
    private Integer id;

    @Column
    private String name;

    public Customer() {
    }

    public Customer(String name) {
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

You can manipulate the Customer entity through a REST endpoint. As a comparison, check this article which uses the standard blocking Hibernate ORM application in Quarkus: Getting started with Quarkus and Hibernate

Here is the RESTEasy Reactive Endpoint written for Quarkus 3.x using Jakarta REST annotations and Mutiny primitives:

package org.acme;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;
import static jakarta.ws.rs.core.Response.Status.*;

import java.util.List;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.RestPath;
import org.hibernate.reactive.mutiny.Mutiny;
import io.smallrye.mutiny.Uni;

@Path("customers")
@ApplicationScoped
@Produces("application/json")
@Consumes("application/json")
public class ExampleResource {

    private static final Logger LOGGER = Logger.getLogger(ExampleResource.class);

    @Inject
    Mutiny.SessionFactory sf;

    @GET
    public Uni<List<Customer>> get() {
        return sf.withTransaction((s, t) -> s
                .createNamedQuery("Customers.findAll", Customer.class)
                .getResultList()
        );
    }

    @GET
    @Path("{id}")
    public Uni<Customer> getSingle(@RestPath Integer id) {
        return sf.withTransaction((s, t) -> s.find(Customer.class, id));
    }

    @POST
    public Uni<Response> create(Customer customer) {
        if (customer == null || customer.getId() != null) {
            throw new WebApplicationException("Id was invalidly set on request.", 422);
        }

        return sf.withTransaction((s, t) -> s.persist(customer))
                .replaceWith(() -> Response.ok(customer).status(CREATED).build());
    }

    @PUT
    @Path("{id}")
    public Uni<Response> update(@RestPath Integer id, Customer customer) {
        if (customer == null || customer.getName() == null) {
            throw new WebApplicationException("Customer name was not set on request.", 422);
        }

        return sf.withTransaction((s, t) -> s.find(Customer.class, id)
            // If entity exists then update it
            .onItem().ifNotNull().invoke(entity -> entity.setName(customer.getName()))
            .onItem().ifNotNull().transform(entity -> Response.ok(entity).build())
            // If entity not found return the appropriate response
            .onItem().ifNull()
            .continueWith(() -> Response.ok().status(NOT_FOUND).build())
        );
    }

    @DELETE
    @Path("{id}")
    public Uni<Response> delete(@RestPath Integer id) {
        return sf.withTransaction((s, t) ->
                s.find(Customer.class, id)
                    // If entity exists then delete it
                    .onItem().ifNotNull()
                        .transformToUni(entity -> s.remove(entity)
                                .replaceWith(() -> Response.ok().status(NO_CONTENT).build()))
                // If entity not found return the appropriate response
                .onItem().ifNull().continueWith(() -> Response.ok().status(NOT_FOUND).build()));
    }
}

When we write persistence logic using Hibernate Reactive, we work with a reactive Mutiny.SessionFactory. Most operations on this interface are non-blocking, and SQL execution against the database is never performed synchronously.

To obtain a reactive Session from the SessionFactory, you can use withSession(). For extra convenience, the withTransaction() method opens a session and starts a transaction in a single call:

public Uni<List<Customer>> get() {
    return sf.withTransaction((s, t) -> s
            .createNamedQuery("Customers.findAll", Customer.class)
            .getResultList()
    );
}

Please note that the reactive session is automatically flushed and closed at the end of the transaction scope.

The reactive Session interface has methods matching standard JPA EntityManager operations (e.g. find(), persist(), createNamedQuery()), making it easy to migrate synchronous JPA logic to reactive workflows.

You can also decorate your Endpoint with a Jakarta REST ExceptionMapper class to output HTTP response errors in JSON format:

package org.acme;

import jakarta.inject.Inject;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.jboss.logging.Logger;

@Provider
public class ErrorMapper implements ExceptionMapper<Exception> {

    private static final Logger LOGGER = Logger.getLogger(ErrorMapper.class);

    @Inject
    ObjectMapper objectMapper;

    @Override
    public Response toResponse(Exception exception) {
        LOGGER.error("Failed to handle request", exception);

        int code = 500;
        if (exception instanceof WebApplicationException) {
            code = ((WebApplicationException) exception).getResponse().getStatus();
        }

        ObjectNode exceptionJson = objectMapper.createObjectNode();
        exceptionJson.put("exceptionType", exception.getClass().getName());
        exceptionJson.put("code", code);

        if (exception.getMessage() != null) {
            exceptionJson.put("error", exception.getMessage());
        }

        return Response.status(code)
                .entity(exceptionJson)
                .build();
    }
}

To connect to PostgreSQL reactively, configure the database connection properties in application.properties:

%prod.quarkus.datasource.db-kind=postgresql
%prod.quarkus.datasource.username=quarkus_test
%prod.quarkus.datasource.password=quarkus_test

quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.sql-load-script=import.sql

%prod.quarkus.datasource.reactive.url=postgresql://localhost/quarkus_test

Notice we configure the non-blocking reactive URL connection using the PostgreSQL Vert.x reactive driver format.

Testing the Quarkus application

To test the application, launch a PostgreSQL database container with Docker or Podman:

docker run -it --rm=true --name quarkus_test -e POSTGRES_USER=quarkus_test -e POSTGRES_PASSWORD=quarkus_test -e POSTGRES_DB=quarkus_test -p 5432:5432 postgres:16

Next, compile and package the Quarkus application:

mvn clean package

After that, run the application in production mode:

java -jar ./target/quarkus-app/quarkus-run.jar

You can also use Quarkus dev mode (mvn quarkus:dev) which automatically spins up a Dev Services PostgreSQL container without needing Docker commands!

Test the reactive endpoint with curl to list all customers:

curl -s http://localhost:8080/customers | jq

Output from the REST GET endpoint:

[
   {
      "id":1,
      "name":"Batman"
   },
   {
      "id":2,
      "name":"Superman"
   },
   {
      "id":3,
      "name":"Wonder woman"
   }
]

You can test adding a new customer via POST request:

curl -X POST http://localhost:8080/customers -H 'Content-Type: application/json' -d '{"name":"Antman"}'

Source code

The source code for this example application (derived from Quarkus’ quickstart application) is available on GitHub at: https://github.com/fmarchioni/mastertheboss/tree/master/quarkus/hibernate-reactive

Frequently Asked Questions (FAQs)

What is the difference between standard Hibernate ORM and Hibernate Reactive in Quarkus 3?

Standard Hibernate ORM relies on JDBC drivers, which use blocking I/O and require dedicated worker threads per database operation. Hibernate Reactive uses non-blocking Vert.x SQL drivers and Mutiny streams, allowing Quarkus to handle high concurrency with fewer underlying threads on event loops.

Can I use Quarkus Panache with Hibernate Reactive?

Yes! Quarkus provides the quarkus-hibernate-reactive-panache extension. It offers Active Record and Repository patterns tailored for non-blocking persistence, returning Mutiny Uni and Multi types for all query and persistence operations.

How are transactions managed in Hibernate Reactive?

Instead of declarative @Transactional annotations (which rely on thread-local transaction contexts), Hibernate Reactive uses programmatically scoped transaction blocks like Mutiny.SessionFactory.withTransaction((session, tx) -> ...) or Reactive Panache's Panache.withTransaction(...).


Recommended Articles

Quarkus 3: Exploring New Reactive REST Features with Mutiny

Discover how Quarkus 3 leverages Mutiny for reactive REST services in modern applications. #Quarkus #ReactiveJava #Middleware #CloudNative

Create a Quarkus Reactive Application with SmallRye Reactive Messaging and Mutiny for Kafka

Learn how to stream data from/to a Kafka cluster using Quarkus, SmallRye Reactive Messaging, and Mutiny. #Quarkus #ReactiveJava #Kafka

Second Tutorial on Messaging with Quarkus and MicroProfile Reactive Messaging

Explore reactive messaging in Quarkus applications using SmallRye Reactive Messaging API. Learn about Message, Incoming, Outgoing annotations and protocol support.

Quarkus: Choosing Between RESTEasy Classic and Reactive for Your New Applications

Learn how to choose between Quarkus RESTEasy Classic and Reactive for your new applications. #Java #Middleware #CloudNative