<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.vertx.howtos</groupId>
<artifactId>hibernate-reactive-howto</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.verticle>io.vertx.howtos.hr.MainVerticle</main.verticle>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-stack-depchain</artifactId>
<version>4.3.7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>vertx-mutiny-clients-bom</artifactId>
<version>3.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-core</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-web</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-pg-client</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.reactive</groupId>
<artifactId>hibernate-reactive-core</artifactId>
<version>1.1.9.Final</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.2</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.6</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.17.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<mainClass>${main.verticle}</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
Vert.x 和 Hibernate Reactive 操作指南
本操作指南将展示如何使用 Hibernate Reactive 构建 Vert.x 服务。
您将构建什么
-
您将构建一个能够列出、获取和记录产品的 HTTP 服务。
-
数据库访问将使用 Hibernate Reactive API 以及 响应式 Vert.x SQL 客户端 完成。
-
为了简化体验,数据库将使用 TestContainers 作为 PostgreSQL 容器启动。
您需要什么
-
文本编辑器或 IDE
-
Java 11 或更高版本
-
Apache Maven
-
Docker
-
HTTPie(用于从命令行发送 HTTP 请求)
创建项目
以下是您应该使用的 pom.xml 文件的内容
pom.xml
该项目使用 Vert.x Mutiny 绑定,因此您将使用 Mutiny 来组合异步操作。
服务实现
服务包含以下 2 个类
-
MainVerticle
包含main
方法以及唯一的 verticle,并且 -
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 | 指定响应式提供程序 |
2 | 确保 Product 将是一个受管理的实体 |
3 | 这是一个 JDBC 风格的 URL,但请放心,它不会使用 JDBC! |
Hibernate Reactive 会选择 Vert.x 响应式 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 API 的 Hibernate Reactive 会话工厂 |
asyncStart
方法需要完成 2 项操作
-
创建一个连接到 PostgreSQL 数据库的 Hibernate Reactive 会话工厂,并且
-
启动一个 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 服务,它具有一个使用 Vert.x 响应式驱动和 Hibernate Reactive 访问数据库的 HTTP API。
-
我们使用了熟悉的 对象关系映射 编程模型,同时仍然具有端到端的响应式请求处理能力。
-
TestContainers 可以用于轻松组装自包含的演示应用程序。