MongoDB 是最可靠、最强大的面向文档的NoSQL 数据库。它允许开发人员提供功能丰富的应用程序和服务,并具有各种现代内置功能,例如机器学习、流媒体、全文搜索等。虽然 MongoDB 不是经典的关系数据库,但它仍然被广泛的不同业务所使用扇区及其用例涵盖各种架构场景和数据类型。

面向文档的数据库本质上与传统的关系型数据库不同,传统的关系型数据库的数据存储在表中,并且单个实体可能分布在多个此类表中。相比之下,文档数据库将数据存储在单独的不相关的集合中,这消除了关系模型固有的沉重性。然而,考虑到现实世界的领域模型从来都不是那么简单地由不相关的单独实体组成,文档数据库(包括 MongoDB)提供了多种定义多集合连接的方法,类似于经典数据库关系,但更轻、更经济、更可靠。效率更高。

Quarkus,“超音速和亚原子”Java 堆栈,是最时尚、最有影响力的开发人员拼命争夺和争夺的新星。其现代化的云原生设施、设计(符合同类最佳标准库)以及构建本机可执行文件的能力吸引了 Java 开发人员、架构师、工程师和软件设计师。年。

这里我们无法详细介绍 MongoDB 或 Quarkus:有兴趣了解更多信息的读者请查看官方文档 MongoDB 网站Quarkus 网站。我们在这里试图实现的是实现一个相对复杂的用例,其中包括使用 Quarkus 及其 MongoDB 扩展对客户订单产品域模型进行 CRUD。为了提供一个受现实世界启发的解决方案,我们试图避免基于零连接单实体模型的简单化和讽刺性示例(现在有数十个)。

所以,我们开始吧!

领域模型

下图显示了我们的 customer-order-product 域模型:
Customer-order-product 域模型

如您所见,模型的中心文档是 Order,存储在名为 Orders 的专用集合中。 OrderOrderItem 文档的聚合,每个文档都指向其关联的 ProductOrder 文档还引用下单的Customer。在 Java 中,其实现如下:

爪哇

 

orderItemSet = new HashSet<>()

}’ data-lang=”text/x-java”>

@MongoEntity(database = "mdb", collection="Orders")
公开课订单
{
  @BsonId
  私人长ID;
  私人 DBRef 客户;
  私人地址送货地址;
  私人地址账单地址;
  私有 Set orderItemSet = new HashSet<>()
  ...
}

在这里,我们需要在订单和下订单的客户之间创建关联。我们可以将关联的 Customer 文档嵌入到 Order 文档中,但这将是一个糟糕的设计,因为它会重复定义同一对象两次。我们需要使用对关联的 Customer 文档的引用,我们使用 DBRef 类来完成此操作。对于一组关联的订单商品,也会发生同样的情况,我们使用一组引用,而不是嵌入文档。

我们的领域模型的其余部分非常相似,并且基于相同的规范化思想;例如,OrderItem 文档:

爪哇

 

@MongoEntity(数据库 = "mdb", collection="OrderItems")
公共类订单项
{
  @BsonId
  私人长ID;
  私有 DBRef 产品;
  私人 BigDecimal 价格;
  私人整数金额;
  ...
}

我们需要关联构成当前订单项对象的产品。最后但并非最不重要的一点是,我们有 Product 文档:

爪哇

 

{
公共 DBRefSerializer()
{
这个(空);
}

受保护的 DBRefSerializer(Class dbrefClass)
{
超级(dbrefClass);
}

@覆盖
公共无效序列化(DBRef dbRef,JsonGenerator jsonGenerator,SerializerProvider serializerProvider)抛出IOException
{
if (dbRef != null)
{
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField(“id”, (String)dbRef.getId());
jsonGenerator.writeStringField(“collectionName”, dbRef.getCollectionName());
jsonGenerator.writeStringField(“databaseName”, dbRef.getDatabaseName());
jsonGenerator.writeEndObject();
}
}
}’ data-lang=”text/x-java”>

公共类 DBRefSerializer 扩展 StdSerializer
{
  公共 DBRefSerializer()
  {
    这个(空);
  }

  受保护的 DBRefSerializer(Class dbrefClass)
  {
    超级(dbrefClass);
  }

  @覆盖
  公共无效序列化(DBRef dbRef,JsonGenerator jsonGenerator,SerializerProvider serializerProvider)抛出IOException
  {
    if (dbRef != null)
    {
      jsonGenerator.writeStartObject();
      jsonGenerator.writeStringField("id", (String)dbRef.getId());
      jsonGenerator.writeStringField("collectionName", dbRef.getCollectionName());
      jsonGenerator.writeStringField("databaseName", dbRef.getDatabaseName());
      jsonGenerator.writeEndObject();
    }
  }
}

这是我们的 DBRef 序列化器,正如您所看到的,它是一个 Jackson 序列化器。这是因为我们在这里使用的 quarkus-mongodb-panache 扩展依赖于 Jackson。也许在未来的版本中,将使用 JSON-B,但目前我们只能使用 Jackson。它照常扩展 StdSerializer 类,并使用 JSON 生成器(作为输入参数传递)序列化其关联的 DBRef 对象,以在输出流上写入 DBRef组件;即对象 ID、集合名称和数据库名称。有关 DBRef 结构的更多信息,请参阅 MongoDB 文档。

解串器正在执行补码操作,如下所示:

爪哇

 

公共类 DBRefDeserializer 扩展 StdDeserializer
{
  公共 DBRefDeserializer()
  {
    这个(空);
  }

  公共 DBRefDeserializer(Class dbrefClass)
  {
    超级(dbrefClass);
  }

   @覆盖
   公共 DBRef deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) 抛出 IOException, JacksonException
   {
     JsonNode 节点 = jsonParser.getCodec().readTree(jsonParser);
     return new DBRef(node.findValue("databaseName").asText(), node.findValue("collectionName").asText(), node.findValue("id").asText());
   }
}

就序列化器/反序列化器而言,这几乎就是所有可以说的了。让我们进一步看看 codecs 包给我们带来了什么。

Java 对象使用 BSON(二进制 JSON)格式存储在 MongoDB 数据库中。为了存储信息,MongoDB 驱动程序需要能够将 Java 对象映射到其关联的 BSON 表示形式。它代表 Codec 接口执行此操作,该接口包含将 Java 对象映射到 BSON 以及相反的映射所需的抽象方法。实现这个接口,可以定义Java和BSON之间的转换逻辑,反之亦然。 MongoDB 驱动程序包含最常见类型所需的 Codec 实现,但同样,由于某种原因,当涉及到 DBRef 时,此实现只是一个虚拟实现,这会引发UnsupportedOperationException。在联系 MongoDB 驱动程序实现者后,除了实现我自己的 Codec 映射器(如 DocstoreDBRefCodec 类所示)之外,我没有成功找到任何其他解决方案。为了简洁起见,我们不会在这里重现此类的源代码。

一旦实现了我们专用的Codec,我们需要将其注册到MongoDB驱动程序,以便在将DBRef类型映射到Java对象时使用它,反之亦然。为此,我们需要实现接口 CoderProvider,如类 DocstoreDBRefCodecProvider 所示,该接口通过其抽象 get() 返回方法,负责执行映射的具体类;即,在我们的例子中,DocstoreDBRefCodec。这就是我们在这里需要做的所有事情,因为 Quarkus 将自动发现并使用我们的 CodecProvider 自定义实现。请查看这些类以了解并理解事情是如何完成的。

数据存储库

Quarkus Panache 通过支持 < a href="https://www.martinfowler.com/eaaCatalog/activeRecord.html" rel="noopener noreferrer" target="_blank">活动记录存储库设计模式。在这里,我们将使用第二个。

与类似的持久性堆栈相反,Panache 依赖于实体的编译时字节码增强。它包括一个自动执行这些增强功能的注释处理器。为了执行其增强工作,该注释处理器所需的只是一个如下所示的接口:

爪哇

 

@ApplicationScoped
公共类 CustomerRepository 实现 PanacheMongoRepositoryBase{}

上面的代码是定义能够持久保存 Customer 文档实例的完整服务所需的全部代码。您的接口需要扩展 PanacheMongoRepositoryBase 并使用您的对象 ID 类型(在我们的示例中为 Long)对其进行参数化。 Panache 注释处理器将生成执行最常见的 CRUD 操作 所需的所有端点,包括但不限于保存、更新、删除、查询、分页、排序、事务处理等。所有这些细节都有完整解释此处。另一种可能性是扩展 PanacheMongoRepository 而不是 PanacheMongoRepositoryBase 并使用提供的 ObjectID 键而不是将它们自定义为 Long ,正如我们在示例中所做的那样。无论您选择第一种还是第二种,这只是一个偏好问题。

REST API

为了使我们的 Panache 生成的持久性服务变得有效,我们需要通过 REST API。在最常见的情况下,我们必须手动制作此 API 及其实现,其中包含所需的全套 REST 端点。通过使用 quarkus-mongodb-rest-data-panache 扩展可以避免这种繁琐且重复的操作,该扩展能够通过具有以下模式的接口自动生成所需的 REST 端点:

爪哇

 

公共接口CustomerResource
  扩展 PanacheMongoRepositoryResource {}

如果您愿意,请相信:这就是生成完整 REST API 实现所需的全部内容,其中包含调用 mongodb-panache 扩展注释处理器之前生成的持久性服务所需的所有端点。现在我们准备将 REST API 构建为 Quarkus 微服务。我们选择将此微服务构建为 Docker 映像,代表 quarkus-container-image-jib 扩展。只需包含以下 Maven 依赖项即可:

XML

 

<依赖项>
  io.quarkus
  quarkus-container-image-jib
  


quarkus-maven-plugin 将创建一个本地 Docker 镜像来运行我们的微服务。该Docker镜像的参数由application.properties文件定义,如下:

属性文件
 
quarkus.container-image.build=true
quarkus.container-image.group=quarkus-nosql-tests
quarkus.container-image.name=docstore-mongodb
quarkus.mongodb.connection-string = mongodb://admin:admin@mongo:27017
quarkus.mongodb.database = mdb
quarkus.swagger-ui.always-include=true
quarkus.jib.jvm-entrypoint=/opt/jboss/container/java/run/run-java.sh

这里我们将新创建的 Docker 镜像的名称定义为 quarkus -nosql-tests/docstore-mongodb。这是由“/”分隔的参数 quarkus.container-image.groupquarkus.container-image.name 的串联。值为 true 的属性 quarkus.container-image.build 指示 Quarkus 插件将构建操作绑定到 package 阶段>maven。这样,只需执行 mvn package 命令,我们就生成了一个能够运行微服务的 Docker 镜像。这可以通过运行 docker images 命令进行测试。名为 quarkus.jib.jvm-entrypoint 的属性定义了新生成的 Docker 映像要运行的命令。 quarkus-run.jar 是基础镜像为 ubi8/openjdk-17-runtime 时使用的 Quarkus 微服务标准启动文件,如我们的例子。其他属性包括 quarkus.mongodb.connection-string 和 quarkus.mongodb.database = mdb,它们定义 MongoDB 数据库连接字符串和数据库名称。最后但并非最不重要的一点是,属性 quarkus.swagger-ui.always-include 在我们的微服务空间中包含了 Swagger UI 界面,以便我们可以轻松地对其进行测试。

现在让我们看看如何运行和测试整个过程。

运行和测试我们的微服务

现在我们已经了解了实现的细节,让我们看看如何运行和测试它。我们选择代表 docker-compose 实用程序来执行此操作。这是关联的 docker-compose.yml 文件:

YAML

 

版本:“3.7”
服务:
  蒙戈:
    图片:蒙戈
    环境:
    MONGO_INITDB_ROOT_USERNAME:管理员
    MONGO_INITDB_ROOT_PASSWORD:管理员
    MONGO_INITDB_DATABASE:mdb
    主机名: 蒙戈
    容器名称:mongo
    端口:
      - “27017:27017”
    卷:
      - ./mongo-init/:/docker-entrypoint-initdb.d/:ro
  蒙戈快递:
    图片:mongo-express
    依赖于取决于:
      - 蒙戈
    主机名: mongo-express
    容器名称:mongo-express
    链接:
      - 蒙戈:蒙戈
    端口:
      - 8081:8081
    环境:
      ME_CONFIG_MONGODB_ADMINUSERNAME:管理员
      ME_CONFIG_MONGODB_ADMINPASSWORD:管理员
      ME_CONFIG_MONGODB_URL:mongodb://admin:admin@mongo:27017/
  文档库:
    图片:quarkus-nosql-tests/docstore-mongodb:1.0-SNAPSHOT
    依赖于取决于:
      - 蒙戈
      - 蒙戈快递
    主机名:文档库
    容器名称:文档库
    链接:
      - 蒙戈:蒙戈
      - 蒙戈快递:蒙戈快递
    端口:
      - “8080:8080”
      - “5005:5005”
    环境:
      JAVA_DEBUG:“正确”
      JAVA_APP_DIR:/home/jboss
      JAVA_APP_JAR:quarkus-run.jar

此文件指示 docker-compose 实用程序运行三个服务:

  • 运行 Mongo DB 7 数据库的名为 mongo 的服务
  • 运行 MongoDB 管理 UI 的名为 mongo-express 的服务
  • 名为 docstore 的服务,运行我们的 Quarkus 微服务

我们应该注意,mongo 服务使用安装在容器的 docker-entrypoint-initdb.d 目录上的初始化脚本。此初始化脚本创建名为 mdb 的 MongoDB 数据库,以便微服务可以使用它。

JavaScript

 

db = db.getSiblingDB(process.env.MONGO_INITDB_ROOT_USERNAME);
数据库.auth(
  process.env.MONGO_INITDB_ROOT_USERNAME,
  process.env.MONGO_INITDB_ROOT_PASSWORD,
);
db = db.getSiblingDB(process.env.MONGO_INITDB_DATABASE);
db.createUser(
{
  用户:“尼古拉斯”,
  密码:“密码1”,
  角色:[
  {
    角色:“dbOwner”,
    数据库:“mdb”
  }]
});
db.createCollection("客户");
db.createCollection("产品");
db.createCollection("订单");
db.createCollection("OrderItems");

这是一个初始化 JavaScript,它创建一个名为 nicolas 的用户和一个名为 mdb 的新数据库。用户拥有数据库的管理权限。还创建了四个新集合,分别名为 CustomersProductsOrdersOrderItems

为了测试微服务,请按以下步骤操作:

  1. 克隆关联的 GitHub 存储库:
$ git 克隆 https://github.com/nicolasduminil/docstore.git

  • 转到项目:
  • $ cd 文档库
    

  • 构建项目:
  • $ mvn 全新安装
    

  • 检查所有必需的 Docker 容器是否正在运行:
  • $ docker ps
    容器 ID   图像                                               命令                  创建         状态         端口                    名称
    
    7882102d404d quarkus-nosql-tests/docstore-mongodb:1.0-SNAPSHOT "/opt/jboss/containe..." 8 秒前 6 秒 0.0.0.0:5005->5005/tcp, :::5005->5005/tcp, 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 8443/tcp 文档库
    
    786fa4fd39d6 mongo-express "/sbin/tini -- /dock…" 8 秒前 向上 7 秒 0.0.0.0:8081->8081/tcp, :::8081->8081/tcp mongo-express
    
    2e850e3233dd mongo "docker-entrypoint.s…" 9 秒前 向上 7 秒 0.0.0.0:27017->27017/tcp, :::27017->27017/tcp mongo
    
    
    

  • 运行集成测试:
  • $ mvn -DskipTests=false 故障安全:集成测试
    

    最后一个命令将运行所有应该成功的集成测试。这些集成测试是使用 RESassured 库实现的。下面的列表显示了位于 docstore-domain 项目中的集成测试之一:

    爪哇

     

    @QuarkusIntegrationTest
    @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
    公共类 CustomerResourceIT
    {
      私人静态客户客户;
    
      @之前所有
      public static void beforeAll() 抛出 AddressException
      {
        客户=新客户(“约翰”,“多伊”,新互联网地址(“john.doe@gmail.com”));
        customer.addAddress(new Address("Gebhard-Gerber-Allee 8", "Kornwestheim", "德国"));
        客户.setId(10L);
      }
    
      @测试
      @订单(10)
      公共无效testCreateCustomerShouldSucceed()
      {
        给定()
          .header("内容类型", "application/json")
          .and().body(客户)
          .when().post("/客户")
          。然后()
          .statusCode(HttpStatus.SC_CREATED);
      }
    
      @测试
      @订单(20)
      公共无效testGetCustomerShouldSucceed()
      {
        断言(给定()
          .header("内容类型", "application/json")
          .when().get("/客户")
          。然后()
          .statusCode(HttpStatus.SC_OK)
          .extract().body().jsonPath().getString("firstName[0]")).isEqualTo("John");
      }
    
      @测试
      @订单(30)
      公共无效testUpdateCustomerShouldSucceed()
      {
        customer.setFirstName(“简”);
        给定()
          .header("内容类型", "application/json")
          .and().body(客户)
          .when().pathParam("id", customer.getId()).put("/customer/{id}")
          。然后()
          .statusCode(HttpStatus.SC_NO_CONTENT);
      }
    
      @测试
      @订单(40)
      公共无效testGetSingleCustomerShouldSucceed()
      {
        断言(给定()
          .header("内容类型", "application/json")
          .when().pathParam("id", customer.getId()).get("/customer/{id}")
          。然后()
          .statusCode(HttpStatus.SC_OK)
          .extract().body().jsonPath().getString("firstName")).isEqualTo("Jane");
      }
    
      @测试
      @订单(50)
      公共无效testDeleteCustomerShouldSucceed()
      {
        给定()
          .header("内容类型", "application/json")
          .when().pathParam("id", customer.getId()).delete("/customer/{id}")
          。然后()
          .statusCode(HttpStatus.SC_NO_CONTENT);
      }
    
      @测试
      @订单(60)
      公共无效testGetSingleCustomerShouldFail()
      {
        给定()
          .header("内容类型", "application/json")
          .when().pathParam("id", customer.getId()).get("/customer/{id}")
          。然后()
          .statusCode(HttpStatus.SC_NOT_FOUND);
      }
    }

    您还可以通过在 http://localhost:8080/q:swagger-ui 上触发您的首选浏览器来使用 Swagger UI 界面进行测试。然后,为了测试端点,您可以使用位于 docstore-api 项目的 src/resources/data 目录中的 JSON 文件中的负载。

    您还可以通过访问 http://localhost:8081 并使用默认凭据 (admin/pass) 进行身份验证来使用 MongoDB UI 管理界面。

    您可以在我的 GitHub 存储库中找到项目源代码。

    享受吧!

    Comments are closed.