Using the MongoDB Client
MongoDB 是一个众所周知且被广泛使用的 NoSQL 数据库。 在本指南中,我们将了解如何让 REST 服务使用 MongoDB 数据库。
- Prerequisites
- Architecture
- Solution
- Creating the Maven project
- Creating your first JSON REST service
- Configuring the MongoDB database
- Multiple MongoDB Clients
- Running a MongoDB Database
- Creating a frontend
- Reactive MongoDB Client
- Simplifying MongoDB Client usage using BSON codec
- The POJO Codec
- Simplifying MongoDB with Panache
- Schema migration with Liquibase
- Connection Health Check
- Metrics
- Tracing
- Testing helpers
- The legacy client
- Building a native executable
- Using mongo+srv:// urls
- Customize the Mongo client configuration programmatically
- Configuration Reference
Solution
我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。
克隆 Git 存储库: git clone $${quickstarts-base-url}.git
,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。
解决方案位于 mongodb-quickstart
directory中。
Creating the Maven project
首先,我们需要一个新项目。使用以下命令创建一个新项目:
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
--no-code
cd {create-app-artifact-id}
要创建一个 Gradle 项目,添加 --gradle
或 --gradle-kotlin-dsl
选项。
有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
-DprojectGroupId={create-app-group-id} \
-DprojectArtifactId={create-app-artifact-id} \
-DnoCode
cd {create-app-artifact-id}
要创建一个 Gradle 项目,添加 -DbuildTool=gradle
或 -DbuildTool=gradle-kotlin-dsl
选项。
适用于 Windows 用户:
-
如果使用 cmd,(不要使用反斜杠
\
,并将所有内容放在同一行上) -
如果使用 Powershell,将
-D
参数用双引号引起来,例如"-DprojectArtifactId={create-app-artifact-id}"
此命令生成一个 Maven 结构,导入 Quarkus REST(以前称为 RESTEasy Reactive)Jackson 和 MongoDB Client 扩展。在此之后,`quarkus-mongodb-client`扩展已添加到构建文件中。
如果你已经配置了 Quarkus 项目,可以通过在项目基本目录中运行以下命令,将 `mongodb-client`扩展添加到项目:
quarkus extension add {add-extension-extensions}
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
./gradlew addExtension --extensions='{add-extension-extensions}'
这会将以下内容添加到您的 pom.xml
:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mongodb-client</artifactId>
</dependency>
implementation("io.quarkus:quarkus-mongodb-client")
Creating your first JSON REST service
在此示例中,我们将创建一个应用程序来管理水果列表。
首先,我们按如下方式创建 `Fruit`bean:
package org.acme.mongodb;
import java.util.Objects;
public class Fruit {
private String name;
private String description;
private String id;
public Fruit() {
}
public Fruit(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Fruit)) {
return false;
}
Fruit other = (Fruit) obj;
return Objects.equals(other.name, this.name);
}
@Override
public int hashCode() {
return Objects.hash(this.name);
}
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
没什么新奇之处。需要特别注意的是,JSON 序列化层要求有缺省构造函数。
现在,创建一个 org.acme.mongodb.FruitService
,它将成为应用程序的业务层,并存储/加载来自 mongoDB 数据库的水果。
package org.acme.mongodb;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import org.bson.Document;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.List;
@ApplicationScoped
public class FruitService {
@Inject MongoClient mongoClient;
public List<Fruit> list(){
List<Fruit> list = new ArrayList<>();
MongoCursor<Document> cursor = getCollection().find().iterator();
try {
while (cursor.hasNext()) {
Document document = cursor.next();
Fruit fruit = new Fruit();
fruit.setName(document.getString("name"));
fruit.setDescription(document.getString("description"));
list.add(fruit);
}
} finally {
cursor.close();
}
return list;
}
public void add(Fruit fruit){
Document document = new Document()
.append("name", fruit.getName())
.append("description", fruit.getDescription());
getCollection().insertOne(document);
}
private MongoCollection getCollection(){
return mongoClient.getDatabase("fruit").getCollection("fruit");
}
}
现在,创建 `org.acme.mongodb.FruitResource`类,如下所示:
@Path("/fruits")
public class FruitResource {
@Inject FruitService fruitService;
@GET
public List<Fruit> list() {
return fruitService.list();
}
@POST
public List<Fruit> add(Fruit fruit) {
fruitService.add(fruit);
return list();
}
}
实现非常简单,你只需要使用 Jakarta REST 注释定义端点,并使用 `FruitService`列出/添加新水果。
Configuring the MongoDB database
要配置的主要属性是访问 MongoDB 的 URL。几乎所有配置都可以包含在连接 URI 中,因此我们建议你这样做。你可以在 MongoDB 文档中找到更多信息:[role="bare"][role="bare"]https://docs.mongodb.com/manual/reference/connection-string/
示例配置如下所示:
# configure the mongoDB client for a replica set of two nodes
quarkus.mongodb.connection-string = mongodb://mongo1:27017,mongo2:27017
在此示例中,我们正在使用运行在 localhost: 上的单个实例:
# configure the mongoDB client for a single instance on localhost
quarkus.mongodb.connection-string = mongodb://localhost:27017
如果你需要更多配置属性,本指南末尾有一个完整列表。
默认情况下,Quarkus 会限制在应用程序内使用 JNDI,作为预防措施,尝试减轻与 Log4Shell 类似的任何将来漏洞。由于 `mongo+srv`协议通常用于连接到 MongoDB 需要 JNDI,因此在使用 MongoDB 客户端扩展时,此保护会自动禁用。
Use the MongoDB Dev Services
请参阅 MongoDB Dev Services。
Multiple MongoDB Clients
MongoDB 允许你配置多个客户端。使用多个客户端与拥有单个客户端的工作方式相同。
quarkus.mongodb.connection-string = mongodb://login:pass@mongo1:27017/database
quarkus.mongodb.users.connection-string = mongodb://mongo2:27017/userdb
quarkus.mongodb.inventory.connection-string = mongodb://mongo3:27017/invdb,mongo4:27017/invdb
请注意,密钥中有多余的部分(users`和 `inventory`段)。语法如下:`quarkus.mongodb.[optional name.][mongo connection property]
。如果省略名称,则配置默认客户端。
使用多个 MongoDB 客户端通过允许连接到多个 MongoDB 集群为 MongoDB 启用多租户。如果你想连接到同一集群中的多个数据库,多个客户端是 *not*必要的,因为单个客户端能够访问同一集群中的所有数据库(就像 JDBC 连接能够访问同一数据库中的多个架构一样)。 |
Named Mongo client Injection
在使用多个客户端时,每个 MongoClient
,你可以使用 `io.quarkus.mongodb.MongoClientName`限定符选择要注入的客户端。使用上述属性配置三个不同的客户端,你还可以按如下方式注入每个客户端:
@Inject
MongoClient defaultMongoClient;
@Inject
@MongoClientName("users")
MongoClient mongoClient1;
@Inject
@MongoClientName("inventory")
ReactiveMongoClient mongoClient2;
Running a MongoDB Database
由于默认情况下,`MongoClient`配置为访问端口为 27017(默认 MongoDB 端口)上的本地 MongoDB 数据库,如果你在此端口上运行本地数据库,在能够测试它之前不必再做任何操作!
如果你想使用 Docker 运行 MongoDB 数据库,可以使用以下命令启动一个:
docker run -ti --rm -p 27017:27017 mongo:4.4
如果您使用 Dev Services,手动启动容器不是必需的。 |
Creating a frontend
现在,让我们添加一个简单的网页与我们的 `FruitResource`Quarkus 交互。Quarkus 会自动提供位置为 `META-INF/resources`目录之下的静态资源。在 `src/main/resources/META-INF/resources`目录中,用其中内容添加一个 `fruits.html`文件,来自该 fruits.html文件。
你现在可以与你的 REST 服务进行交互:
-
start Quarkus with:include::./_includes/devtools/dev.adoc[]
-
通过表单向列表中添加新水果
Reactive MongoDB Client
Quarkus 中包含一个响应式 MongoDB 客户端。使用它与使用传统 MongoDB 客户端一样容易。您可以重写之前的示例以使用它,比如以下示例。
Mutiny
MongoDB 响应式客户端使用 Mutiny 响应式类型。如果您不熟悉 Mutiny,请查看 Mutiny - an intuitive reactive programming library。 |
package org.acme.mongodb;
import io.quarkus.mongodb.reactive.ReactiveMongoClient;
import io.quarkus.mongodb.reactive.ReactiveMongoCollection;
import io.smallrye.mutiny.Uni;
import org.bson.Document;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.List;
@ApplicationScoped
public class ReactiveFruitService {
@Inject
ReactiveMongoClient mongoClient;
public Uni<List<Fruit>> list() {
return getCollection().find()
.map(doc -> {
Fruit fruit = new Fruit();
fruit.setName(doc.getString("name"));
fruit.setDescription(doc.getString("description"));
return fruit;
}).collect().asList();
}
public Uni<Void> add(Fruit fruit) {
Document document = new Document()
.append("name", fruit.getName())
.append("description", fruit.getDescription());
return getCollection().insertOne(document)
.onItem().ignore().andContinueWithNull();
}
private ReactiveMongoCollection<Document> getCollection() {
return mongoClient.getDatabase("fruit").getCollection("fruit");
}
}
package org.acme.mongodb;
import io.smallrye.mutiny.Uni;
import java.util.List;
import jakarta.inject.Inject;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.core.MediaType;
@Path("/reactive_fruits")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ReactiveFruitResource {
@Inject
ReactiveFruitService fruitService;
@GET
public Uni<List<Fruit>> list() {
return fruitService.list();
}
@POST
public Uni<List<Fruit>> add(Fruit fruit) {
return fruitService.add(fruit)
.onItem().ignore().andSwitchTo(this::list);
}
}
Simplifying MongoDB Client usage using BSON codec
通过使用 Bson Codec
,MongoDB 客户端将自动处理您的领域对象与 MongoDB `Document`之间的转换。
首先,您需要创建一个 Bson Codec
,它将告诉 Bson 如何从您的实体到 MongoDB Document`转换以及如何进行反向转换。我们在这里使用 `CollectibleCodec
,因为我们的对象可以从数据库进行检索(它具有 MongoDB 标识符),如果不是这样,我们使用 `Codec`即可。有关编解码器文档的更多信息:[role="bare"]https://www.mongodb.com/docs/drivers/java/sync/current/fundamentals/data-formats/codecs/。
package org.acme.mongodb.codec;
import com.mongodb.MongoClientSettings;
import org.acme.mongodb.Fruit;
import org.bson.Document;
import org.bson.BsonWriter;
import org.bson.BsonValue;
import org.bson.BsonReader;
import org.bson.BsonString;
import org.bson.codecs.Codec;
import org.bson.codecs.CollectibleCodec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import java.util.UUID;
public class FruitCodec implements CollectibleCodec<Fruit> {
private final Codec<Document> documentCodec;
public FruitCodec() {
this.documentCodec = MongoClientSettings.getDefaultCodecRegistry().get(Document.class);
}
@Override
public void encode(BsonWriter writer, Fruit fruit, EncoderContext encoderContext) {
Document doc = new Document();
doc.put("name", fruit.getName());
doc.put("description", fruit.getDescription());
documentCodec.encode(writer, doc, encoderContext);
}
@Override
public Class<Fruit> getEncoderClass() {
return Fruit.class;
}
@Override
public Fruit generateIdIfAbsentFromDocument(Fruit document) {
if (!documentHasId(document)) {
document.setId(UUID.randomUUID().toString());
}
return document;
}
@Override
public boolean documentHasId(Fruit document) {
return document.getId() != null;
}
@Override
public BsonValue getDocumentId(Fruit document) {
return new BsonString(document.getId());
}
@Override
public Fruit decode(BsonReader reader, DecoderContext decoderContext) {
Document document = documentCodec.decode(reader, decoderContext);
Fruit fruit = new Fruit();
if (document.getString("id") != null) {
fruit.setId(document.getString("id"));
}
fruit.setName(document.getString("name"));
fruit.setDescription(document.getString("description"));
return fruit;
}
}
然后,您需要创建一个 `CodecProvider`将此 `Codec`链接到 `Fruit`类。
package org.acme.mongodb.codec;
import org.acme.mongodb.Fruit;
import org.bson.codecs.Codec;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
public class FruitCodecProvider implements CodecProvider {
@Override
public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
if (clazz.equals(Fruit.class)) {
return (Codec<T>) new FruitCodec();
}
return null;
}
}
Quarkus 会处理以 @Singleton`范围的 CDI bean 的形式为您注册 `CodecProvider
。
最后,当从数据库获取 `MongoCollection`时,您可以直接使用 `Fruit`类而不是 `Document`类,编解码器会将 `Document`类与 `Fruit`类自动进行映射。
以下是使用 `FruitCodec`的 `MongoCollection`的示例。
package org.acme.mongodb;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.List;
@ApplicationScoped
public class CodecFruitService {
@Inject MongoClient mongoClient;
public List<Fruit> list(){
List<Fruit> list = new ArrayList<>();
MongoCursor<Fruit> cursor = getCollection().find().iterator();
try {
while (cursor.hasNext()) {
list.add(cursor.next());
}
} finally {
cursor.close();
}
return list;
}
public void add(Fruit fruit){
getCollection().insertOne(fruit);
}
private MongoCollection<Fruit> getCollection(){
return mongoClient.getDatabase("fruit").getCollection("fruit", Fruit.class);
}
}
The POJO Codec
POJO Codec 提供一组注释,启用对 POJO 映射到 MongoDB 集合的方式进行自定义,而这个编解码器由 Quarkus 自动初始化
其中一个注释是 @BsonDiscriminator
注释,它允许通过在文档中添加鉴别器字段,以在单个 MongoDB 集合中存储多个 Java 类型。当处理抽象类型或接口时,它会很有用。
Quarkus 会自动使用 POJO 编解码器注册所有使用 @BsonDiscriminator
注释的类。
POJO 编解码器通过 PropertyCodecProvider
增强了泛型支持,Quarkus 会使用 POJO 编解码器自动注册任何 PropertyCodecProvider
(这些类自动做成 @Singleton
范围的 CDI bean)。在构建本机可执行文件以及使用泛型类型时,您可能需要为反射注册类型参数。
Simplifying MongoDB with Panache
MongoDB with Panache 扩展通过提供类似于 Hibernate ORM with Panache中的活动记录实体(和存储库)促进了 MongoDB 的使用,并且专注于让您的实体编写起来轻松有趣。
Schema migration with Liquibase
Liquibase MongoDB 扩展促进了 MongoDB 数据库的初始化,包括索引和初始数据。它实现了与 Liquibase 为 SQL 数据库提供的相同模式迁移工具。
Connection Health Check
如果您使用 quarkus-smallrye-health
扩展,quarkus-mongodb-client
会自动添加一个准备就绪健康检查以验证与群集的连接。
因此,当您访问应用程序的 /q/health/ready
端点时,您将获得有关连接验证状态的信息。
可以通过在 application.properties
中将 quarkus.mongodb.health.enabled
属性设置为 false
来禁用此行为。
Metrics
如果您正在使用 quarkus-micrometer
或 quarkus-smallrye-metrics
扩展,quarkus-mongodb-client
可以提供有关连接池的指标。必须先通过在 application.properties
中将 quarkus.mongodb.metrics.enabled
属性设置为 true
来启用此行为。
因此,当您访问应用程序的 /q/metrics
端点时,您将获得有关连接池状态的信息。在使用 SmallRye Metrics 时,可以 vendor
在范围内找到连接池指标。
Tracing
要对 MongoDB 使用追踪,需要将 quarkus-opentelemetry
扩展添加到你的项目中。
即使所有追踪基础设施都已到位,MongoDB 追踪也不会默认启用,你需要通过设置以下属性来启用:
# enable tracing
quarkus.mongodb.tracing.enabled=true
Testing helpers
Dev Services for MongoDB 是为单元测试启动 MongoDB 数据库的最佳选择。
但如果你无法使用它,则可以使用 Quarkus 提供的两个 QuarkusTestResourceLifecycleManager
之一启动 MongoDB 数据库。它们依赖 Flapdoodle embedded MongoDB。
-
io.quarkus.test.mongodb.MongoTestResource
会在端口 27017 上启动单个实例。 -
io.quarkus.test.mongodb.MongoReplicaSetTestResource
会使用两个实例启动副本集,一个在端口 27017 上,另一个在端口 27018 上。
要使用它们,你需要将 io.quarkus:quarkus-test-mongodb
依赖项添加到你的 pom.xml。
要了解更多关于使用 QuarkusTestResourceLifecycleManager
的信息,请参阅 Quarkus test resource。
要设置在启动时 MongoDB 将侦听的目标端口,应使用以下代码:
要设置将启动的目标 MongoDB 版本,应使用以下代码:
所使用的字符串值可以是 |
The legacy client
我们默认不包含传统 MongoDB 客户端。它包含现已停用的 MongoDB Java API(DB、DBCollection、……),而 com.mongodb.MongoClient
现在已被 com.mongodb.client.MongoClient
取代。
如果你希望使用传统 API,则需要将以下依赖添加到你的构建文件中:
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-legacy</artifactId>
</dependency>
implementation("org.mongodb:mongodb-driver-legacy")
Building a native executable
你可以在原生可执行文件中使用 MongoDB 客户端。
如果您想使用 SSL/TLS 加密,则需要在 `application.properties`中添加这些属性:
quarkus.mongodb.tls=true
quarkus.mongodb.tls-insecure=true # only if TLS certificate cannot be validated
然后可以使用以下命令构建可执行的原始文件:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
运行起来跟执行 `./target/mongodb-quickstart-1.0.0-SNAPSHOT-runner`一样简单。
然后您可以将浏览器指向 http://localhost:8080/fruits.html
,并使用您的应用程序。
目前,Quarkus 不支持在原生模式下的 Client-Side Field Level Encryption。
如果您在以原生模式运行应用程序时遇到以下错误: |
Using mongo+srv:// urls
`mongo+srv://`URL 在 JVM 模式下开箱即用。但是,在原生模式中,由 MongoDB 客户端提供的默认 DNS 解析器使用 JNDI,并且在原生模式下不起作用。
如果您需要在原生模式下使用 mongo+srv://
,则可以配置一个备用 DNS 解析器。此功能正在 experimental,它可能会导致 JVM 应用程序和原生应用程序之间存在差异。
若要启用替代 DNS 解析器,请使用:
quarkus.mongodb.native.dns.use-vertx-dns-resolver=true
如属性名称所示,它使用 Vert.x 检索 DNS 记录。默认情况下,它会尝试从 /etc/resolv.conf`读取第一个 `nameserver
,如果此文件存在的话。您也可以配置您的 DNS 服务器:
quarkus.mongodb.native.dns.use-vertx-dns-resolver=true
quarkus.mongodb.native.dns.server-host=10.0.0.1
quarkus.mongodb.native.dns.server-port=53 # 53 is the default port
此外,您还可以使用以下方法配置查找超时:
quarkus.mongodb.native.dns.use-vertx-dns-resolver=true
quarkus.mongodb.native.dns.lookup-timeout=10s # the default is 5s
Customize the Mongo client configuration programmatically
如果您需要以编程方式定制 Mongo 客户端配置,则需要实现 `io.quarkus.mongodb.runtime.MongoClientCustomizer`接口并将其公开为 CDI _application scoped_bean:
@ApplicationScoped
public class MyCustomizer implements MongoClientCustomizer {
@Override
public MongoClientSettings.Builder customize(MongoClientSettings.Builder builder) {
return builder.applicationName("my-app");
}
}
该 bean 可以使用 `@MongoClientName`限定符来自定义特定客户端,以指示客户端名称。当没有限定符时,它会自定义默认客户端。每个客户端至多可以使用一个定制器。如果检测到针对同一客户端的多个定制器,则会在构建时引发异常。
此功能可用于配置客户端侧字段级加密 (CSFLE)。请按照 the Mongo web site中的说明配置 CSFLE:
@ApplicationScoped
public class MyCustomizer implements MongoClientCustomizer {
@Override
public MongoClientSettings.Builder customize(MongoClientSettings.Builder builder) {
Map<String, Map<String, Object>> kmsProviders = getKmsProviders();
String dek = getDataEncryptionKey();
Map<String, BsonDocument> schema = getSchema(dek);
Map<String, Object> extraOptions = new HashMap<>();
extraOptions.put("cryptSharedLibPath", "<path to crypt shared library>");
return builder.autoEncryptionSettings(AutoEncryptionSettings.builder()
.keyVaultNamespace(KEY_VAULT_NAMESPACE)
.kmsProviders(kmsProviders)
.schemaMap(schemaMap)
.extraOptions(extraOptions)
.build());
}
}
一般而言,客户端侧字段级加密和依赖于 Mongo Crypt的功能都不支持原生模式。