Vert.x and Hibernate Reactive How-to

This how-to shows how to build a Vert.x service with Hibernate Reactive.

What you will build

What you need

  • A text editor or IDE

  • Java 11 or higher

  • Apache Maven

  • Docker

  • HTTPie to make HTTP requests from the command-line

Create a project

Here is the content of the pom.xml file that you should be using:

The project uses the Vert.x Mutiny bindings, so you will use Mutiny to compose asynchronous operations.

Service implementation

The service fits in 2 classes:

  1. MainVerticle contains the main method as well as the sole verticle, and

  2. Product is an entity to be managed with Hibernate.

Product entity

Let’s start with the Product entity. A product has a primary key, a name and a price:

package io.vertx.howtos.hr;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.math.BigDecimal;

@Entity
public class Product {

  @Id
  @GeneratedValue
  private Long id;

  @Column(unique = true)
  private String name;

  @Column(nullable = false)
  private BigDecimal price;

  public Product() {
  }

  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 BigDecimal getPrice() {
    return price;
  }

  public void setPrice(BigDecimal price) {
    this.price = price;
  }
}

Hibernate Reactive configuration

The configuration of Hibernate Reactive is not very different from regular Hibernate.

We need a META-INF/persistence.xml file as:

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">

  <persistence-unit name="pg-demo">
    <provider>org.hibernate.reactive.provider.ReactivePersistenceProvider</provider>  <!--(1)-->

    <class>io.vertx.howtos.hr.Product</class> <!--(2)-->

    <properties>

      <!-- PostgreSQL -->
      <property name="javax.persistence.jdbc.url"
                value="jdbc:postgresql://localhost/postgres"/>  <!--(3)-->

      <!-- Credentials -->
      <property name="javax.persistence.jdbc.user"
                value="postgres"/>
      <property name="javax.persistence.jdbc.password"
                value="vertx-in-action"/>

      <!-- The Vert.x SQL Client connection pool size -->
      <property name="hibernate.connection.pool_size"
                value="10"/>

      <!-- Automatic schema export -->
      <property name="javax.persistence.schema-generation.database.action"
                value="drop-and-create"/>

      <!-- SQL statement logging -->
      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>
      <property name="hibernate.highlight_sql" value="true"/>

    </properties>

  </persistence-unit>

</persistence>
  1. Specify the reactive provider

  2. Make sure Product will be a managed entity

  3. This is a JDBC-style URL, but be assured that it won’t use JDBC!

Hibernate Reactive selects the Vert.x reactive PostgreSQL driver because the persistence.xml file uses a PostgreSQL URL, and because io.smallrye.reactive:smallrye-mutiny-vertx-pg-client brings the driver implementation on the classpath.

Application startup

First of all, we start a PostgreSQL container with TestContainers in the main method, as in:

PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>("postgres:11-alpine")
  .withDatabaseName("postgres")
  .withUsername("postgres")
  .withPassword("vertx-in-action");

postgreSQLContainer.start();

Once the container is available, we can create a Vertx context and deploy MainVerticle:

Vertx vertx = Vertx.vertx();

DeploymentOptions options = new DeploymentOptions().setConfig(new JsonObject()
  .put("pgPort", postgreSQLContainer.getMappedPort(5432))); // (1)

vertx.deployVerticle(MainVerticle::new, options).subscribe().with(  // (2)
  ok -> {
    long vertxTime = System.currentTimeMillis();
    logger.info("✅ Deployment success");
    logger.info("💡 PostgreSQL container started in {}ms", (tcTime - startTime));
    logger.info("💡 Vert.x app started in {}ms", (vertxTime - tcTime));
  },
  err -> logger.error("🔥 Deployment failure", err));
  1. We need to pass the PostgreSQL port that is mapped outside the container

  2. Because we are using Mutiny, we need to subscribe to the deployment operation and actually trigger it, otherwise nothing happens

Verticle setup

The verticle in class MainVerticle uses the Mutiny bindings, so there is a Mutiny-friendly method to define the start behavior. This method call asyncStart returns a Uni<Void>:

public class MainVerticle extends AbstractVerticle {

  private static final Logger logger = LoggerFactory.getLogger(MainVerticle.class);
  private Mutiny.SessionFactory emf;  // (1)

  @Override
  public Uni<Void> asyncStart() {
  1. This is the Hibernate Reactive session factory with a Mutiny-based API

The asyncStart method needs to complete 2 operations:

  1. create an Hibernate Reactive session factory connected to the PostgreSQL database, and

  2. start a HTTP server.

We can do these operations simultaneously rather than in sequence.

The session factory is created using the following code, where we override the "JDBC" URL with the PostgreSQL port since it it dynamically allocated. Also note that creating session factory is a blocking operation, so we offload it to a work thread.

Uni<Void> startHibernate = Uni.createFrom().deferred(() -> {
  var pgPort = config().getInteger("pgPort", 5432);
  var props = Map.of("javax.persistence.jdbc.url", "jdbc:postgresql://localhost:" + pgPort + "/postgres");  // (1)

  emf = Persistence
    .createEntityManagerFactory("pg-demo", props)
    .unwrap(Mutiny.SessionFactory.class);

  return Uni.createFrom().voidItem();
});

startHibernate = vertx.executeBlocking(startHibernate)  // (2)
  .onItem().invoke(() -> logger.info("✅ Hibernate Reactive is ready"));
  1. Override the PostgreSQL port

  2. Offload to a worker thread

The HTTP API is defined using 3 routes: 1 to get all products, 1 to get a specific product, and 1 to create / record a new product:

Router router = Router.router(vertx);

BodyHandler bodyHandler = BodyHandler.create();
router.post().handler(bodyHandler::handle);

router.get("/products").respond(this::listProducts);
router.get("/products/:id").respond(this::getProduct);
router.post("/products").respond(this::createProduct);

We can finally start the HTTP server then await for Hibernate Reactive to be ready:

Uni<HttpServer> startHttpServer = vertx.createHttpServer()
  .requestHandler(router::handle)
  .listen(8080)
  .onItem().invoke(() -> logger.info("✅ HTTP server listening on port 8080"));

return Uni.combine().all().unis(startHibernate, startHttpServer).discardItems();  // (1)
  1. Join the 2 asynchronous operations, then discard the values and return a Void value, hence Uni<Void>

Once the 2 operations have completed then the asyncStart method reports that the verticle deployment has completed.

Request handling methods

The methods to handle HTTP requests use Hibernate Reactive for database access:

private Uni<List<Product>> listProducts(RoutingContext ctx) {
  return emf.withSession(session -> session
    .createQuery("from Product", Product.class)
    .getResultList());
}

private Uni<Product> getProduct(RoutingContext ctx) {
  long id = Long.parseLong(ctx.pathParam("id"));
  return emf.withSession(session -> session
    .find(Product.class, id))
    .onItem().ifNull().continueWith(Product::new);
}

private Uni<Product> createProduct(RoutingContext ctx) {
  Product product = ctx.getBodyAsJson().mapTo(Product.class);
  return emf.withSession(session -> session.
    persist(product)
    .call(session::flush)
    .replaceWith(product));
}

These are standard Hibernate operations (e.g., persist, flush, find) chained using Mutiny operators (e.g., chain, onItem, replaceWith).

Running the application

The application is self-contained as it starts a PostgreSQL container. You can use Maven to compile and run the application:

$ mvn compile exec:java

The logs should be similar to:

[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ hibernate-reactive-howto ---
2021-05-18 13:54:35,570 INFO [io.vertx.howtos.hr.MainVerticle.main()] io.vertx.howtos.hr.MainVerticle - 🚀 Starting a PostgreSQL container
2021-05-18 13:54:39,430 DEBUG [io.vertx.howtos.hr.MainVerticle.main()] 🐳 [postgres:11-alpine] - Starting container: postgres:11-alpine
2021-05-18 13:54:39,431 DEBUG [io.vertx.howtos.hr.MainVerticle.main()] 🐳 [postgres:11-alpine] - Trying to start container: postgres:11-alpine (attempt 1/1)
2021-05-18 13:54:39,432 DEBUG [io.vertx.howtos.hr.MainVerticle.main()] 🐳 [postgres:11-alpine] - Starting container: postgres:11-alpine
2021-05-18 13:54:39,432 INFO [io.vertx.howtos.hr.MainVerticle.main()] 🐳 [postgres:11-alpine] - Creating container for image: postgres:11-alpine
2021-05-18 13:54:40,016 INFO [io.vertx.howtos.hr.MainVerticle.main()] 🐳 [postgres:11-alpine] - Starting container with ID: f538f59149d72ebec87382b09624240ca2faddcbc9c247a53575a537d1d7f045
2021-05-18 13:54:42,050 INFO [io.vertx.howtos.hr.MainVerticle.main()] 🐳 [postgres:11-alpine] - Container postgres:11-alpine is starting: f538f59149d72ebec87382b09624240ca2faddcbc9c247a53575a537d1d7f045
2021-05-18 13:54:43,918 INFO [io.vertx.howtos.hr.MainVerticle.main()] 🐳 [postgres:11-alpine] - Container postgres:11-alpine started in PT4.510869S
2021-05-18 13:54:43,918 INFO [io.vertx.howtos.hr.MainVerticle.main()] io.vertx.howtos.hr.MainVerticle - 🚀 Starting Vert.x
2021-05-18 13:54:44,342 INFO [vert.x-worker-thread-0] org.hibernate.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: pg-demo]
2021-05-18 13:54:44,408 INFO [vert.x-worker-thread-0] org.hibernate.Version - HHH000412: Hibernate ORM core version 5.4.31.Final
2021-05-18 13:54:44,581 INFO [vert.x-eventloop-thread-0] io.vertx.howtos.hr.MainVerticle - ✅ HTTP server listening on port 8080
2021-05-18 13:54:44,586 INFO [vert.x-worker-thread-0] org.hibernate.annotations.common.Version - HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2021-05-18 13:54:44,775 INFO [vert.x-worker-thread-0] org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.PostgreSQL10Dialect
2021-05-18 13:54:45,006 INFO [vert.x-worker-thread-0] o.h.reactive.provider.impl.ReactiveIntegrator - HRX000001: Hibernate Reactive Preview
2021-05-18 13:54:45,338 INFO [vert.x-worker-thread-0] o.h.reactive.pool.impl.DefaultSqlClientPool - HRX000011: SQL Client URL [jdbc:postgresql://localhost:55019/postgres]
2021-05-18 13:54:45,342 WARN [vert.x-worker-thread-0] io.vertx.core.impl.VertxImpl - You're already on a Vert.x context, are you sure you want to create a new Vertx instance?
2021-05-18 13:54:45,345 INFO [vert.x-worker-thread-0] o.h.reactive.pool.impl.DefaultSqlClientPool - HRX000012: Connection pool size: 10
[Hibernate]
    drop table if exists Product cascade
2021-05-18 13:54:45,521 WARN [vert.x-eventloop-thread-0] io.vertx.sqlclient.impl.SocketConnectionBase - Backend notice: severity='NOTICE', code='00000', message='table "product" does not exist, skipping', detail='null', hint='null', position='null', internalPosition='null', internalQuery='null', where='null', file='tablecmds.c', line='1060', routine='DropErrorMsgNonExistent', schema='null', table='null', column='null', dataType='null', constraint='null'
[Hibernate]
    drop sequence if exists hibernate_sequence
2021-05-18 13:54:45,527 WARN [vert.x-eventloop-thread-0] io.vertx.sqlclient.impl.SocketConnectionBase - Backend notice: severity='NOTICE', code='00000', message='sequence "hibernate_sequence" does not exist, skipping', detail='null', hint='null', position='null', internalPosition='null', internalQuery='null', where='null', file='tablecmds.c', line='1060', routine='DropErrorMsgNonExistent', schema='null', table='null', column='null', dataType='null', constraint='null'
[Hibernate]
    drop table if exists Product cascade
2021-05-18 13:54:45,537 WARN [vert.x-eventloop-thread-0] io.vertx.sqlclient.impl.SocketConnectionBase - Backend notice: severity='NOTICE', code='00000', message='table "product" does not exist, skipping', detail='null', hint='null', position='null', internalPosition='null', internalQuery='null', where='null', file='tablecmds.c', line='1060', routine='DropErrorMsgNonExistent', schema='null', table='null', column='null', dataType='null', constraint='null'
[Hibernate]
    drop sequence if exists hibernate_sequence
2021-05-18 13:54:45,540 WARN [vert.x-eventloop-thread-0] io.vertx.sqlclient.impl.SocketConnectionBase - Backend notice: severity='NOTICE', code='00000', message='sequence "hibernate_sequence" does not exist, skipping', detail='null', hint='null', position='null', internalPosition='null', internalQuery='null', where='null', file='tablecmds.c', line='1060', routine='DropErrorMsgNonExistent', schema='null', table='null', column='null', dataType='null', constraint='null'
[Hibernate]
    create sequence hibernate_sequence start 1 increment 1
[Hibernate]
    create table Product (
       id int8 not null,
        name varchar(255),
        price numeric(19, 2) not null,
        primary key (id)
    )
[Hibernate]
    alter table if exists Product
       add constraint UK_gxubutkbk5o2a6aakbe7q9kww unique (name)
2021-05-18 13:54:45,574 INFO [vert.x-eventloop-thread-0] io.vertx.howtos.hr.MainVerticle - ✅ Hibernate Reactive is ready
2021-05-18 13:54:45,576 INFO [vert.x-eventloop-thread-1] io.vertx.howtos.hr.MainVerticle - ✅ Deployment success
2021-05-18 13:54:45,576 INFO [vert.x-eventloop-thread-1] io.vertx.howtos.hr.MainVerticle - 💡 PostgreSQL container started in 8349ms
2021-05-18 13:54:45,576 INFO [vert.x-eventloop-thread-1] io.vertx.howtos.hr.MainVerticle - 💡 Vert.x app started in 1658ms

We can insert products:

$ http :8080/products name="Baguette" price="1.20"
HTTP/1.1 200 OK
content-length: 39
content-type: application/json; charset=utf-8

{
    "id": 1,
    "name": "Baguette",
    "price": 1.2
}

$ http :8080/products name="Pain" price="1.40"
HTTP/1.1 200 OK
content-length: 35
content-type: application/json; charset=utf-8

{
    "id": 2,
    "name": "Pain",
    "price": 1.4
}

We can also list a specific product:

$ http :8080/products/1
HTTP/1.1 200 OK
content-length: 39
content-type: application/json; charset=utf-8

{
    "id": 1,
    "name": "Baguette",
    "price": 1.2
}

And we can of course list all products:

$ http :8080/products
HTTP/1.1 200 OK
content-length: 77
content-type: application/json; charset=utf-8

[
    {
        "id": 1,
        "name": "Baguette",
        "price": 1.2
    },
    {
        "id": 2,
        "name": "Pain",
        "price": 1.4
    }
]

Since we enabled Hibernate SQL logging, the application logs show the requests being executed such as:

[Hibernate]
    select
        product0_.id as id1_0_0_,
        product0_.name as name2_0_0_,
        product0_.price as price3_0_0_
    from
        Product product0_
    where
        product0_.id=$1

Summary

  • We built a Vert.x service with a HTTP API that accesses a database using a Vert.x reactive driver and Hibernate Reactive.

  • We used the familiar object-relational mapping programming model while still having end-to-end reactive requests processing.

  • TestContainers can be used to easily assemble self-contained demo applications.


Last published: 2021-12-22 01:17:00 +0000.