Quarkus - 反应式 SQL 客户端
反应式 SQL 客户端有一个简单的 API,侧重于可伸缩和低损耗。 目前支持以下数据库服务器:
-
PostgreSQL
-
MariaDB/MySQL
在本指南中,您将学习如何实现一个简单的 CRUD 应用程序,这个应用程序将在 RESTful API 上暴露存储在 PostgreSQL 中的数据。
每种客户端的 Extension 和连接池类名在本文档底部找到。 |
如果您不熟悉 Quarkus Vert.x extension ,请先阅读 使用 Eclipse Vert.x 指南。 |
应用会管理 fruit 实体:
public class Fruit {
public Long id;
public String name;
public Fruit() {
}
public Fruit(String name) {
this.name = name;
}
public Fruit(Long id, String name) {
this.id = id;
this.name = name;
}
}
您需要一个马上能用的 PostgreSQL 服务器来尝试示例吗?
|
安装
Reactive PostgreSQL Client extension
首先,请确保您的项目已启用 quarkus-reactive-pg-client
扩展。
如果您正在创建一个新项目,请将 extensions
参数设置为:
mvn io.quarkus:quarkus-maven-plugin:1.3.1.Final:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=reactive-pg-client-quickstart \
-Dextensions="reactive-pg-client"
cd reactive-pg-client-quickstart
如果您已经创建了一个项目,可以使用 add-extension
命令将 reactive-pg-client
extension 添加到现有的 Quarkus 项目中:
./mvnw quarkus:add-extension -Dextensions="reactive-pg-client"
否则,你可以手动将这个添加到 pom.xml
文件的依赖部分:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
在本指南中,我们将使用 Reactive PostgreSQL Client 的 Axle API。 阅读 使用 Eclipse Vert.x 指南以了解callback、Mutiny、RxJava 和 Axle 的API之间的差异。 RxJava 和 Axle API 已废弃,计划移除。 建议切换到Mutiny。 |
Mutiny
推荐的 API 使用 Mutiny 反应式类型,如果你不熟悉它们,请先阅读 反应式入门指南 。 |
JSON 绑定
我们会通过 JSON 格式通过 HTTP 暴露 Fruit
实例。
因此,您还需要添加 quarkus-resteasy-jsonb
扩展:
./mvnw quarkus:add-extension -Dextensions="resteasy-jsonb"
如果你不想使用命令行,请手动将其添加到你的 pom.xml
文件的依赖部分:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>
当然,这只是本指南的要求,而不是任何使用 Reactive PostgreSQL Client 的应用程序都需要。
配置
Reactive PostgreSQL Client 可以使用标准的 Quarkus 数据源属性和一个反应式 URL:
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus_test
quarkus.datasource.password=quarkus_test
quarkus.datasource.reactive.url=postgresql://localhost:5432/quarkus_test
你可以创建 FruitResource
骨架并 @Inject
一个 io.vertx.mutiny.pgclient.PgPool
实例:
@Path("fruits")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class FruitResource {
@Inject
io.vertx.mutiny.pgclient.PgPool client;
}
数据库表结构和种子数据
在实现 REST 接口和数据管理代码之前,我们需要配置数据库表结构。 事先插入一些数据也会更方便。
对于生产环境,我们建议使用 Flyway数据库迁移工具。 但为了开发,我们可以在启动时简单地删除重建表,然后插入几条 fruits 。
@Inject
@ConfigProperty(name = "myapp.schema.create", defaultValue = "true") (1)
boolean schemaCreate;
@PostConstruct
void config() {
if (schemaCreate) {
initdb();
}
}
private void initdb() {
// TODO
}
}
您可以覆盖 application.properties 文件中的 myapp.schema.create 属性的默认值。
|
即将准备就绪!
要初始化开发模式中的DB,我们将使用简单的 query
方法。
它返回 Uni
,因此可以由它组成按顺序执行查询:
client.query("DROP TABLE IF EXISTS fruits")
.flatMap(r -> client.query("CREATE TABLE fruits (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"))
.flatMap(r -> client.query("INSERT INTO fruits (name) VALUES ('Orange')"))
.flatMap(r -> client.query("INSERT INTO fruits (name) VALUES ('Pear')"))
.flatMap(r -> client.query("INSERT INTO fruits (name) VALUES ('Apple')"))
.await().indefinitely();
想知道为什么我们需要阻塞直到完成最新查询?
此代码是 @PostConstruct 方法的一部分,Quarkus 同步调用。
如果过早返回,可能导致在数据库尚未准备就绪的情况下处理请求。
|
就这些了! 到目前为止,我们已经看到如何配置一个 pooled client 并执行简单的查询。 我们现在准备开发数据管理代码并执行我们的 RESTful 接口。
使用
遍历查询结果
在开发模式下,数据库已经在 fruits
表中有几行记录。
要检索所有数据,我们将再次使用 query
方法:
Uni<RowSet> rowSet = client.query("SELECT id, name FROM fruits ORDER BY name ASC");
当操作完成,我们将会得到一个 RowSet
,所有行都可以在内存中缓冲。
作为一个 java.lang.Iterable<Row>
, 它可以经过一个 for-each 循环:
Uni<List<Fruit>> fruits = rowSet
.map(pgRowSet -> {
List<Fruit> list = new ArrayList<>(pgRowSet.size());
for (Row row : pgRowSet) {
list.add(from(row));
}
return list;
});
from
方法将一个 Row
实例转换为 Fruit
实例。
为了便于采用其他数据管理方法:
private static Fruit from(Row row) {
return new Fruit(row.getLong("id"), row.getString("name"));
}
把它放在一起, Fruit.findAll
方法看起来就像:
public static Uni<List<Fruit>> findAll(PgPool client) {
return client.query("SELECT id, name FROM fruits ORDER BY name ASC")
.map(pgRowSet -> {
List<Fruit> list = new ArrayList<>(pgRowSet.size());
for (Row row : pgRowSet) {
list.add(from(row));
}
return list;
});
}
从后端获取所有 fruits 的接口:
@GET
public Uni<Response> get() {
return Fruit.findAll(client)
.map(Response::ok)
.map(ResponseBuilder::build);
}
现在以 dev
模式启动夸库斯:
./mvnw compile quarkus:dev
最后,打开您的浏览器并访问 http://localhost:8080/fruits, 您应该看到:
[{"id":3,"name":"Apple"},{"id":1,"name":"Orange"},{"id":2,"name":"Pear"}]
准备查询
Reactive PostgreSQL Client 也可以准备查询和执行时替换 SQL 语句中的参数:
client.preparedQuery("SELECT name FROM fruits WHERE id = $1", Tuple.of(id))
SQL 字符串可以通过位置引用参数,使用 $1,$2,…等。 |
就像简单的 query
方法一样, preparedQuery
返回一个 Uni<RowSet>
的实例。
装备了这个工具后,我们能够安全地使用用户提供的 id
来获取指定 fruit 的详细信息:
public static Uni<Fruit> findById(PgPool client, Long id) {
return client.preparedQuery("SELECT id, name FROM fruits WHERE id = $1", Tuple.of(id)) (1)
.map(RowSet::iterator) (2)
.map(iterator -> iterator.hasNext() ? from(iterator.next()) : null); (3)
}
1 | 创建一个 Tuple 以保存准备的查询参数。 |
2 | 获取一个 RowSet 结果的 Iterator 。 |
3 | 如果发现一个实体,从 Row 创建一个 Fruit 实例。 |
在 JAX-RS 接口中:
@GET
@Path("{id}")
public Uni<Response> getSingle(@PathParam Long id) {
return Fruit.findById(client, id)
.map(fruit -> fruit != null ? Response.ok(fruit) : Response.status(Status.NOT_FOUND)) (1)
.map(ResponseBuilder::build); (2)
}
1 | 准备使用 Fruit 实例或 404 状态代码的 JAX-RS 响应。 |
2 | 构建并发送响应。 |
保存 Fruit
时适用相同的逻辑:
public Uni<Long> save(PgPool client) {
return client.preparedQuery("INSERT INTO fruits (name) VALUES ($1) RETURNING (id)", Tuple.of(name))
.map(pgRowSet -> pgRowSet.iterator().next().getLong("id"));
}
在 web 接口中,我们处理了 POST
请求:
@POST
public Uni<Response> create(Fruit fruit) {
return fruit.save(client)
.map(id -> URI.create("/fruits/" + id))
.map(uri -> Response.created(uri).build());
}
Result metadata
一个 RowSet
不仅在内存中保存您的数据,它还为您提供了一些有关数据本身的信息,例如:
-
受查询影响的行数(根据查询类型插入/删除/更新/检索),
-
列名称。
Let’s use this to support removal of fruits in the database:
public static Uni<Boolean> delete(PgPool client, Long id) {
return client.preparedQuery("DELETE FROM fruits WHERE id = $1", Tuple.of(id))
.map(pgRowSet -> pgRowSet.rowCount() == 1); (1)
}
1 | 检查元数据以确定水果是否确实被删除。 |
Web 接口中的 HTTP DELETE
方法:
@DELETE
@Path("{id}")
public Uni<Response> delete(@PathParam Long id) {
return Fruit.delete(client, id)
.map(deleted -> deleted ? Status.NO_CONTENT : Status.NOT_FOUND)
.map(status -> Response.status(status).build());
}
使用 GET
、POST
和 DELETE
方法后,我们现在可以创建一个最小的网页来尝试使用 RESTful 接口。
我们将使用 jQuery 来简化与后端的交互:
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Reactive PostgreSQL Client - Quarkus</title>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script type="application/javascript" src="fruits.js"></script>
</head>
<body>
<h1>Fruits API Testing</h1>
<h2>All fruits</h2>
<div id="all-fruits"></div>
<h2>Create Fruit</h2>
<input id="fruit-name" type="text">
<button id="create-fruit-button" type="button">Create</button>
<div id="create-fruit"></div>
</body>
</html>
在 Javascript 代码中,我们需要一个函数来刷新水果列表,如果:
-
页面已加载,或
-
添加 fruit,或
-
fruit 已删除。
function refresh() {
$.get('/fruits', function (fruits) {
var list = '';
(fruits || []).forEach(function (fruit) { (1)
list = list
+ '<tr>'
+ '<td>' + fruit.id + '</td>'
+ '<td>' + fruit.name + '</td>'
+ '<td><a href="#" onclick="deleteFruit(' + fruit.id + ')">Delete</a></td>'
+ '</tr>'
});
if (list.length > 0) {
list = ''
+ '<table><thead><th>Id</th><th>Name</th><th></th></thead>'
+ list
+ '</table>';
} else {
list = "No fruits in database"
}
$('#all-fruits').html(list);
});
}
function deleteFruit(id) {
$.ajax('/fruits/' + id, {method: 'DELETE'}).then(refresh);
}
$(document).ready(function () {
$('#create-fruit-button').click(function () {
var fruitName = $('#fruit-name').val();
$.post({
url: '/fruits',
contentType: 'application/json',
data: JSON.stringify({name: fruitName})
}).then(refresh);
});
refresh();
});
1 | 当数据库为空时, fruits 参数没有定义。 |
全部完成! 访问 http://localhost:8080/resours.html 并读取/创建/删除一些水果。
Database Clients 详细信息
数据库 | 扩展名 | 池类名称 |
---|---|---|
PostgreSQL |
|
|
MariaDB/MySQL |
|
|
配置参考
通用数据源
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Type |
Default |
|
---|---|---|
The kind of database we will connect to (e.g. h2, postgresql…). |
string |
|
Whether or not an health check is published in case the smallrye-health extension is present. This is a global setting and is not specific to a datasource. |
boolean |
|
Whether or not datasource metrics are published in case the smallrye-metrics extension is present. This is a global setting and is not specific to a datasource. NOTE: This is different from the "jdbc.enable-metrics" property that needs to be set on the JDBC datasource level to enable collection of metrics for that datasource. |
boolean |
|
The datasource username |
string |
|
The datasource password |
string |
|
The credentials provider name |
string |
|
The credentials provider type.
It is the |
string |
|
int |
|
|
Type |
Default |
|
The kind of database we will connect to (e.g. h2, postgresql…). |
string |
|
The datasource username |
string |
|
The datasource password |
string |
|
The credentials provider name |
string |
|
The credentials provider type.
It is the |
string |
|
int |
|
反应式数据源
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Type |
Default |
|
---|---|---|
If we create a Reactive datasource for this datasource. |
boolean |
|
The datasource URL. |
string |
|
The datasource pool maximum size. |
int |
MariaDB/MySQL
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Type |
Default |
|
---|---|---|
Whether prepared statements should be cached on the client side. |
boolean |
|
Charset for connections. |
string |
|
Collation for connections. |
string |