Using the MongoDB Client

Prerequisites

include::./_includes/prerequisites.adoc[]* 安装了 MongoDB 或安装了 Docker

Architecture

本指南中的应用程序非常简单:用户可以使用窗体在列表中添加元素,然后更新列表。

浏览器和服务器之间所有信息都采用 JSON 格式。

元素存储在 MongoDB 中。

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

解决方案位于 mongodb-quickstart directory中。

Creating the Maven project

首先,我们需要一个新项目。使用以下命令创建一个新项目:

CLI
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 指南。

Maven
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`扩展添加到项目:

CLI
quarkus extension add {add-extension-extensions}
Maven
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
Gradle
./gradlew addExtension --extensions='{add-extension-extensions}'

这会将以下内容添加到您的 pom.xml

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-mongodb-client</artifactId>
</dependency>
build.gradle
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 服务进行交互:

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-micrometerquarkus-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 将侦听的目标端口,应使用以下代码:

@WithTestResource(value = MongoTestResource.class, initArgs = @ResourceArg(name = MongoTestResource.PORT, value = "27017"))

要设置将启动的目标 MongoDB 版本,应使用以下代码:

@WithTestResource(value = MongoTestResource.class, initArgs = @ResourceArg(name = MongoTestResource.VERSION, value = "V5_0"))

所使用的字符串值可以是 de.flapdoodle.embed.mongo.distribution.Versionde.flapdoodle.embed.mongo.distribution.Version.Main 枚举中的任意一个。如果没有指定版本,则默认使用 Version.Main.V4_0

The legacy client

我们默认不包含传统 MongoDB 客户端。它包含现已停用的 MongoDB Java API(DB、DBCollection、……),而 com.mongodb.MongoClient 现在已被 com.mongodb.client.MongoClient 取代。

如果你希望使用传统 API,则需要将以下依赖添加到你的构建文件中:

pom.xml
<dependency>
    <groupId>org.mongodb</groupId>
    <artifactId>mongodb-driver-legacy</artifactId>
</dependency>
build.gradle
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

然后可以使用以下命令构建可执行的原始文件:

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./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

如果您在以原生模式运行应用程序时遇到以下错误:Failed to encode 'MyObject'. Encoding 'myVariable' errored with: Can’t find a codec for class org.acme.MyVariable.,则表示 GraalVM 不知道 `org.acme.MyVariable`类,解决办法是向 `MyVariable`类添加 `@RegisterForReflection`注解。可以在 native application tips页面上找到关于 `@RegisterForReflection`注解的更多详情。

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的功能都不支持原生模式。

Configuration Reference

Unresolved include directive in modules/ROOT/pages/mongodb.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-mongodb-client.adoc[]