最佳实践:MongoDB Quarkus 中的 Panache 实体一对多关系处理?

Best practice: MongoDB Panache entity one-to-many relationship handling in Quarkus?

Quarkus Guide on using MongoDB with Panache does unfortunately not mention what is considered the best practice dealing with one-to-many relations of entities. Note: I would like to model the dependent sub-document, but as an entity on its own. The MongoDB site demonstrates this pattern in: Model One-to-Many Relationships with Document References 这样您就必须在存储库中查找链接到父 ID 的所有实体。

Quarkus(Panache)目前有没有提供更方便查找的方法?

PS: 好像有一个open enhancement request from May 2020可以自动获取引用的实体。

MongoDB是非关系型数据库,所以没有一对多关系支持。

I would like to model the dependent sub-document,

我没听懂,支持子文档,你可以创建一个单独的POJO并将其用作你实体的属性。

不支持的是自动将引用的实体从一个集合加载到另一个集合,正如您指出的那样,有一个开放的增强请求支持它,您可以对此提供反馈(或通过 +1 投票) ).

目前,对于引用的实体,您需要创建两个实体/存储库并手动进行fecth。

正如@loicmathieu 所告知的那样,在 quarkus 中没有直接的方法可以做到 populate(),因为您可以在 mongoose 中做到这一点。 如果你设置 mongoose.set('debug', true) 那么你可以先在控制台中检查

  • find 执行集合查询
  • in 执行参考集合查询。

示例 json 文档:

{
    "_id" : ObjectId("612f6c5c081d0796761ec2e4"),
    "content" : "cannot restart Mongodb even when memory space is available",
    "created_by" : ObjectId("612cb8608d22afb0e404adf6"),
    "tags" : [ 
        ObjectId("612f6c5c081d0796761ec2e2")
    ],
    "createdAt" : ISODate("2021-09-01T12:04:44.361Z"),
    "updatedAt" : ISODate("2021-09-10T05:30:51.384Z"),
    "__v" : 2,
    "likes" : [],
    "replies" : []
}

标记文件

{
    "_id" : ObjectId("612f6c5c081d0796761ec2e2"),
    "name" : "mongodb",
    "created_by" : ObjectId("612cb8608d22afb0e404adf6"),
    "createdAt" : ISODate("2021-09-01T12:04:44.358Z"),
    "updatedAt" : ISODate("2021-09-01T12:04:44.358Z"),
    "__v" : 0
}

最后是用户文档

{
    "_id" : ObjectId("612cb8608d22afb0e404adf6"),
    "username" : "silentsudo",
    "password" : "sosimplebcrypted",
    "name" : "Ashish",
    "__v" : 0
}

要获取我执行以下查询的所有文档:

exports.getRecentTweets = async () => {
    return Tweet.find()
        .populate('created_by', '_id name username mediaId')
        .populate('tags', '_id name')
        .sort({'createdAt': -1});
}

Mongoose 在内部以下列方式执行此操作

Mongoose: tweets.find({}, { sort: { createdAt: -1 }, projection: {} })
Mongoose: users.find({ _id: { '$in': [ new ObjectId("612cb8608d22afb0e404adf6"), new ObjectId("612f57ca9ea5c5cabbd89b38"), new ObjectId("612f57ae9ea5c5cabbd89b32"), new ObjectId("6134cb55328b793a1dacb9c2"), new ObjectId("612cb86a8d22afb0e404adfc") ] }}, { skip: undefined, limit: undefined, perDocumentLimit: undefined, projection: { _id: 1, name: 1, username: 1, mediaId: 1 }})
Mongoose: tags.find({ _id: { '$in': [ new ObjectId("6144d9ef02f6da33f39fe134"), new ObjectId("612f6c5c081d0796761ec2e2"), new ObjectId("612f6e9f081d0796761ec343"), new ObjectId("612dc4623c401072c8098a84"), new ObjectId("612f6cb1081d0796761ec2f1"), new ObjectId("612f6c72081d0796761ec2eb"), new ObjectId("612f6e5b081d0796761ec337"), new ObjectId("612f6e42081d0796761ec330"), new ObjectId("612f6df9081d0796761ec320"), new ObjectId("612f6e0a081d0796761ec328"), new ObjectId("612f6de8081d0796761ec316"), new ObjectId("612f6de8081d0796761ec319"), new ObjectId("612f6dc9081d0796761ec30c"), new ObjectId("612f6dc9081d0796761ec30f"), new ObjectId("612f6db0081d0796761ec303"), new ObjectId("612f6db0081d0796761ec306"), new ObjectId("612f6cc7081d0796761ec2f7") ] }}, { skip: undefined, limit: undefined, perDocumentLimit: undefined, projection: { _id: 1, name: 1 }})

我正在试验 ab tool 进行基准测试。所以我决定使用 quarkus,因为我以前从未使用它构建过任何生产应用程序。所以我快速创建了模型并到达了这个线程才知道没有直接的方法来实现基于 java 的驱动程序中的填充。但是我们可以将 $lookup 与我们需要的聚合管道一起使用。第一种也是最简单的方法是使用 mongodb 罗盘工具构建查询,然后简单地以 java 语言导出结果。我的 json 查询生成类似于填充查询的输出,如下所示。

[
  {
    '$unwind': {
      'path': '$tags', 
      'preserveNullAndEmptyArrays': true
    }
  }, {
    '$lookup': {
      'from': 'tags', 
      'localField': 'tags', 
      'foreignField': '_id', 
      'as': 'tweet_tags'
    }
  }, {
    '$unwind': {
      'path': '$tweet_tags', 
      'preserveNullAndEmptyArrays': true
    }
  }, {
    '$group': {
      '_id': '$_id', 
      'content': {
        '$first': '$content'
      }, 
      'user': {
        '$first': '$created_by'
      }, 
      'tags': {
        '$push': '$tweet_tags'
      }
    }
  }, {
    '$lookup': {
      'from': 'users', 
      'localField': 'user', 
      'foreignField': '_id', 
      'as': 'createdByUser'
    }
  }, {
    '$unwind': {
      'path': '$createdByUser', 
      'preserveNullAndEmptyArrays': false
    }
  }, {
    '$set': {
      'createdByUser.password': null
    }
  }
]

转换成等价物 java 得到

public static List<Document> getTweetByUserQuery() {
            return Arrays.asList(new Document("$unwind",
                            new Document("path", "$tags")
                                    .append("preserveNullAndEmptyArrays", true)),
                    new Document("$lookup",
                            new Document("from", "tags")
                                    .append("localField", "tags")
                                    .append("foreignField", "_id")
                                    .append("as", "tweet_tags")),
                    new Document("$unwind",
                            new Document("path", "$tweet_tags")
                                    .append("preserveNullAndEmptyArrays", true)),
                    new Document("$group",
                            new Document("_id", "$_id")
                                    .append("content",
                                            new Document("$first", "$content"))
                                    .append("user",
                                            new Document("$first", "$created_by"))
                                    .append("tags",
                                            new Document("$push", "$tweet_tags"))),
                    new Document("$lookup",
                            new Document("from", "users")
                                    .append("localField", "user")
                                    .append("foreignField", "_id")
                                    .append("as", "createdByUser")),
                    new Document("$unwind",
                            new Document("path", "$createdByUser")
                                    .append("preserveNullAndEmptyArrays", false)),
                    new Document("$set",
                            new Document("createdByUser.password",
                                    new BsonNull())));
        }

quarkus控制器代码如下:

@Path("/hello")
public class ExampleResource {
    @Inject
    private MongoClient mongoClient;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Map<Object, Object> hello() {
        MongoCollection<Tweet> tweetsCollection = mongoClient.getDatabase("simple-node-js")
                .getCollection("tweets", Tweet.class);


        List<Document> groupByQueryUser = Tweet.GroupByIdDetails.getTweetByUserQuery();

        MongoCollection<Document> collections = mongoClient.getDatabase("simple-node-js")
                .getCollection("tweets");
        AggregateIterable<Document> aggregate = collections.aggregate(groupByQueryUser);
        List<Document> data = new ArrayList<>();
        for (Document value : aggregate) {
            System.out.println(value);
            data.add(value);
        }

        return Map.of("size", data.size(), "data", data);
    }


}

响应还需要做很多其他事情,比如删除不需要的字段,但通过这种方式我可以在 node+mongoose 上实现相同的结果api(除了我在这里跳过的一些事情)