Vert.x 和 Hibernate Reactive 操作指南

本操作指南展示了如何使用 Hibernate Reactive 构建 Vert.x 服务。

您将构建什么

您需要什么

  • 文本编辑器或 IDE

  • Java 11 或更高版本

  • Apache Maven

  • Docker

  • HTTPie,用于从命令行发起 HTTP 请求

创建项目

以下是您应使用的 pom.xml 文件内容

该项目使用 Vert.x Mutiny 绑定,因此您将使用 Mutiny 来组合异步操作。

服务实现

该服务包含 2 个类

  1. MainVerticle 包含 main 方法以及唯一的 verticle,并且

  2. Product 是使用 Hibernate 管理的实体。

Product 实体

我们从 Product 实体开始。一个 产品 具有主键、名称和价格

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 配置

Hibernate Reactive 的配置与 普通 Hibernate 的配置没有太大区别。

我们需要一个 META-INF/persistence.xml 文件,如下所示:

<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:///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. 指定 reactive 提供程序

  2. 确保 Product 将是一个受管理的实体

  3. 这是一个 JDBC 风格的 URL,但请放心,它不会使用 JDBC!

Hibernate Reactive 选择 Vert.x reactive PostgreSQL 驱动程序,因为 persistence.xml 文件使用 PostgreSQL URL,并且因为 io.smallrye.reactive:smallrye-mutiny-vertx-pg-client 将驱动程序实现引入了类路径。

应用程序启动

首先,我们在 main 方法中启动一个 TestContainers PostgreSQL 容器,如下所示:

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

postgreSQLContainer.start();

容器可用后,我们可以创建一个 Vertx 上下文并部署 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. 我们需要传递映射到容器外部的 PostgreSQL 端口

  2. 因为我们使用的是 Mutiny,所以我们需要订阅部署操作并实际触发它,否则什么都不会发生

Verticle 设置

MainVerticle 中的 verticle 使用 Mutiny 绑定,因此有一个 Mutiny 友好的方法来定义启动行为。此方法调用 asyncStart 返回一个 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. 这是带有 Mutiny-based API 的 Hibernate Reactive 会话工厂

asyncStart 方法需要完成 2 项操作

  1. 创建一个连接到 PostgreSQL 数据库的 Hibernate Reactive 会话工厂,以及

  2. 启动一个 HTTP 服务器。

我们可以同时执行这些操作,而不是按顺序执行。

会话工厂使用以下代码创建,其中我们用 PostgreSQL 端口覆盖了“JDBC”URL,因为它会动态分配。另请注意,创建会话工厂是一个阻塞操作,因此我们将其卸载到工作线程。

Uni<Void> startHibernate = Uni.createFrom().deferred(() -> {
  var pgPort = config().getInteger("pgPort", 5432);
  var props = Map.of("javax.persistence.jdbc.url", "jdbc:postgresql://:" + 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. 覆盖 PostgreSQL 端口

  2. 卸载到工作线程

HTTP API 使用 3 个路由定义:1 个用于获取所有产品,1 个用于获取特定产品,1 个用于创建/记录新产品

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

我们最终可以启动 HTTP 服务器,然后等待 Hibernate Reactive 准备就绪

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

return Uni.combine().all().unis(startHibernate, startHttpServer).discardItems();  // (1)
  1. 连接这 2 个异步操作,然后丢弃这些值并返回一个 Void 值,因此是 Uni<Void>

一旦这 2 个操作完成,则 asyncStart 方法报告 verticle 部署已完成。

请求处理方法

处理 HTTP 请求的方法使用 Hibernate Reactive 进行数据库访问

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.body().asPojo(Product.class);
  return emf.withSession(session -> session.
    persist(product)
    .call(session::flush)
    .replaceWith(product));
}

这些是标准的 Hibernate 操作(例如,persist, flush, find),使用 Mutiny 操作符(例如,chain, onItem, replaceWith)链式调用。

运行应用程序

该应用程序是自包含的,因为它会启动一个 PostgreSQL 容器。您可以使用 Maven 编译并运行该应用程序

$ mvn compile exec:java

日志应类似于

[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://: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

我们可以插入产品

$ 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
}

我们也可以列出特定产品

$ 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
}

当然,我们也可以列出所有产品

$ 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
    }
]

由于我们启用了 Hibernate SQL 日志记录,应用程序日志会显示正在执行的请求,例如

[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

总结

  • 我们构建了一个 Vert.x 服务,其 HTTP API 使用 Vert.x reactive 驱动程序和 Hibernate Reactive 访问数据库。

  • 我们使用了熟悉的 对象关系映射 编程模型,同时仍然拥有端到端的 reactive 请求处理。

  • TestContainers 可用于轻松组装自包含的演示应用程序。

另请参阅


最后发布时间:2025-02-03 01:24:48 +0000。