如何查询 MongoDB 中的引用对象?

IT技术 javascript node.js mongodb
2021-01-29 17:31:52

我的 Mongo 数据库中有两个集合,Foos 包含对一个或多个Bars 的引用

Foo: { 
  prop1: true,
  prop2: true,
  bars: [
     {
     "$ref": "Bar",
     "$id": ObjectId("blahblahblah")
     }
  ]
}

Bar: {
   testprop: true
}

我想要的是找到所有Foo至少有一个Bar将其 testprop 设置为 trues 我试过这个命令,但它没有返回任何结果:

db.Foo.find({ "bars.testprop" : { "$in": [ true ] } })

有任何想法吗?

5个回答

您现在可以在 Mongo 3.2 中使用 $lookup

$lookup 需要四个参数

from: 指定要与之执行连接的同一数据库中的集合。from 集合不能被分片。

localField: 指定从文档输入到 $lookup 阶段的字段。$lookup 对来自 from 集合的文档的 localField 与 foreignField 执行相等匹配。

foreignField: 指定来自 from 集合中文档的字段。

as: 指定要添加到输入文档的新数组字段的名称。新的数组字段包含来自 from 集合的匹配文档。

db.Foo.aggregate(
  {$unwind: "$bars"},
  {$lookup: {
    from:"bar",
    localField: "bars",
    foreignField: "_id",
    as: "bar"

   }},
   {$match: {
    "bar.testprop": true
   }}
)
localField: "bars.$id"考虑到 OP 的数据结构,这不需要吗?
2021-03-13 17:31:52
这个答案需要解释。
2021-03-14 17:31:52
@Pat 您应该将其更改为已接受的答案
2021-03-19 17:31:52
如何在 mongodb shell 中查看上述查询的结果?@sidgate
2021-03-24 17:31:52

你不能。http://www.mongodb.org/display/DOCS/Database+References

您必须在客户端中执行此操作。

答案通常是将数据非规范化为父对象的子元素。它增加了维护非规范化记录的开销,但允许这种查询模式。
2021-03-12 17:31:52
@Pat 你现在可以在 3.2 中查询看看下面我的回答
2021-03-15 17:31:52
嗯……废话。那太糟糕了。谢谢。
2021-03-22 17:31:52
@Pat 你们每个人都找到了问题的答案吗?如果是这样,你能提供代码吗,我在这方面有点挣扎
2021-03-28 17:31:52
@brianScroggins 答案基本上是“做不到”。相反,我最终重新设计了我的数据建模方式。不幸的是,我仍然手头没有代码。
2021-04-11 17:31:52

我们遇到了类似的问题,因为我们将 MongoDB(3.4.4,实际上是 3.5.5 用于测试)与我们@Referenece在几个实体上使用的 Morphia 结合使用。我们对这个解决方案并不满意,正在考虑删除这些声明,而是手动进行引用查找。

即我们有一个公司集合和一个用户集合。 Morphia 中的用户实体包含@Refrence对公司实体声明。相应的公司集合包含以下条目:

/* 1 */
{
    "_id" : ObjectId("59a92501df01110fbb6a5dee"),
    "name" : "Test",
    "gln" : "1234567890123",
    "uuid" : "f1f86961-e8d5-40bb-9d3f-fdbcf549066e",
    "creationDate" : ISODate("2017-09-01T09:14:41.551Z"),
    "lastChange" : ISODate("2017-09-01T09:14:41.551Z"),
    "version" : NumberLong(1),
    "disabled" : false
}

/* 2 */
{
    "_id" : ObjectId("59a92501df01110fbb6a5def"),
    "name" : "Sample",
    "gln" : "3210987654321",
    "uuid" : "fee69ee4-b29c-483b-b40d-e702b50b0451",
    "creationDate" : ISODate("2017-09-01T09:14:41.562Z"),
    "lastChange" : ISODate("2017-09-01T09:14:41.562Z"),
    "version" : NumberLong(1),
    "disabled" : false
}

而用户集合包含以下条目:

/* 1 */
{
    "_id" : ObjectId("59a92501df01110fbb6a5df0"),
    "userId" : "admin",
    "userKeyEncrypted" : {
        "salt" : "78e0528db239fd86",
        "encryptedAttribute" : "e4543ddac7cca9757721379e4e70567bb13956694f473b73f7723ac2e2fc5245"
    },
    "passwordHash" : "$2a$10$STRNORu9rcbq4qYUMld4G.HJk8QQQQBmAswSNC/4PBn2bih0BvjM6",
    "roles" : [ 
        "ADMIN"
    ],
    "company" : {
        "$ref" : "company",
        "$id" : ObjectId("59a92501df01110fbb6a5dee")
    },
    "uuid" : "b8aafdcf-d5c4-4040-a96d-8ab1a8608af8",
    "creationDate" : ISODate("2017-09-01T09:14:41.673Z"),
    "lastChange" : ISODate("2017-09-01T09:14:41.765Z"),
    "version" : NumberLong(1),
    "disabled" : false
}

/* 2 */
{
    "_id" : ObjectId("59a92501df01110fbb6a5df1"),
    "userId" : "sample",
    "userKeyEncrypted" : {
        "salt" : "e3ac48695dea5f51",
        "encryptedAttribute" : "e804758b0fd13c219c3fc383eaa9267b70f7b8a1ed74f05575add713ce11804a"
    },
    "passwordHash" : "$2a$10$Gt2dq1vy4J9MeqDnXjokAOtvFcvbhe/g9wAENXFPaPxLAw1L4EULG",
    "roles" : [ 
        "USER"
    ],
    "company" : {
        "$ref" : "company",
        "$id" : ObjectId("59a92501df01110fbb6a5def")
    },
    "uuid" : "55b62d4c-e5ee-408d-80c0-b79e02085b02",
    "creationDate" : ISODate("2017-09-01T09:14:41.873Z"),
    "lastChange" : ISODate("2017-09-01T09:14:41.878Z"),
    "version" : NumberLong(1),
    "disabled" : false
}

/* 3 */
{
    "_id" : ObjectId("59a92501df01110fbb6a5df2"),
    "userId" : "user",
    "userKeyEncrypted" : {
        "salt" : "ab9df671340a7d8b",
        "encryptedAttribute" : "7d8ad4ca6ad88686d810c70498407032f1df830596f72d931880483874d9cce3"
    },
    "passwordHash" : "$2a$10$0FLFw3ixW79JIBrD82Ly6ebOwnEDliS.e7GmrNkFp2nkWDA9OE/RC",
    "uuid" : "d02aef94-fc3c-4539-a22e-e43b8cd78aaf",
    "creationDate" : ISODate("2017-09-01T09:14:41.991Z"),
    "lastChange" : ISODate("2017-09-01T09:14:41.995Z"),
    "version" : NumberLong(1),
    "disabled" : false
}

为了创建一个特殊的公司用户视图,我们还想取消对用户中的公司的引用,并且只包含选定的字段。根据错误报告中的评论,我们了解到 MongoDB 提供了一种$objectToArray: "$$ROOT.element"操作,操作基本上将给定元素的字段拆分为键和值对。注意$objectToArray操作是在 MongoDB 3.4.4 版本中添加的!

使用该$objectToArray操作对包含在用户集合中的 company 元素进行聚合可能如下所示:

dp.user.aggregate([{ 
    $project: { 
        "userId": 1, 
        "userKeyEncrypted": 1, 
        "uuid":1, 
        "roles": 1, 
        "passwordHash": 1, 
        "disabled": 1, 
        company: { $objectToArray: "$$ROOT.company" }
    } 
}])

上述聚合的结果如下所示:

/* 1 */
{
    "_id" : ObjectId("59a92501df01110fbb6a5df0"),
    "userId" : "admin",
    "userKeyEncrypted" : {
        "salt" : "78e0528db239fd86",
        "encryptedAttribute" : "e4543ddac7cca9757721379e4e70567bb13956694f473b73f7723ac2e2fc5245"
    },
    "passwordHash" : "$2a$10$STRNORu9rcbq4qYUMld4G.HJk8QQQQBmAswSNC/4PBn2bih0BvjM6",
    "roles" : [ 
        "ADMIN"
    ],
    "uuid" : "b8aafdcf-d5c4-4040-a96d-8ab1a8608af8",
    "disabled" : false,
    "company" : [ 
        {
            "k" : "$ref",
            "v" : "company"
        }, 
        {
            "k" : "$id",
            "v" : ObjectId("59a92501df01110fbb6a5dee")
        }
    ]
}

/* 2 */
{
    "_id" : ObjectId("59a92501df01110fbb6a5df1"),
    "userId" : "sample",
    "userKeyEncrypted" : {
        "salt" : "e3ac48695dea5f51",
        "encryptedAttribute" : "e804758b0fd13c219c3fc383eaa9267b70f7b8a1ed74f05575add713ce11804a"
    },
    "passwordHash" : "$2a$10$Gt2dq1vy4J9MeqDnXjokAOtvFcvbhe/g9wAENXFPaPxLAw1L4EULG",
    "roles" : [ 
        "USER"
    ],
    "uuid" : "55b62d4c-e5ee-408d-80c0-b79e02085b02",
    "disabled" : false,
    "company" : [ 
        {
            "k" : "$ref",
            "v" : "company"
        }, 
        {
            "k" : "$id",
            "v" : ObjectId("59a92501df01110fbb6a5def")
        }
    ]
}

/* 3 */
{
    "_id" : ObjectId("59a92501df01110fbb6a5df2"),
    "userId" : "user",
    "userKeyEncrypted" : {
        "salt" : "ab9df671340a7d8b",
        "encryptedAttribute" : "7d8ad4ca6ad88686d810c70498407032f1df830596f72d931880483874d9cce3"
    },
    "passwordHash" : "$2a$10$0FLFw3ixW79JIBrD82Ly6ebOwnEDliS.e7GmrNkFp2nkWDA9OE/RC",
    "uuid" : "d02aef94-fc3c-4539-a22e-e43b8cd78aaf",
    "disabled" : false,
    "company" : null
}

现在只需过滤不需要的内容(即没有分配公司的用户并选择正确的数组条目),以便提供$lookup@sidgate 已经解释操作并将取消引用的公司的值复制到用户响应中。

即像下面这样的聚合将执行连接并将公司的数据添加到将公司分配为as查找中定义值的用户

db.user.aggregate([
    { $project: { "userId": 1, "userKeyEncrypted": 1, "uuid":1, "roles": 1, "passwordHash": 1, "disabled": 1, company: { $objectToArray: "$$ROOT.company" }} }, 
    { $unwind: "$company" }, 
    { $match: { "company.k": "$id"}  }, 
    { $lookup: { from: "company", localField: "company.v", foreignField: "_id", as: "company_data" } }
])

上述聚合的结果如下所示:

/* 1 */
{
    "_id" : ObjectId("59a92501df01110fbb6a5df0"),
    "userId" : "admin",
    "userKeyEncrypted" : {
        "salt" : "78e0528db239fd86",
        "encryptedAttribute" : "e4543ddac7cca9757721379e4e70567bb13956694f473b73f7723ac2e2fc5245"
    },
    "passwordHash" : "$2a$10$STRNORu9rcbq4qYUMld4G.HJk8QQQQBmAswSNC/4PBn2bih0BvjM6",
    "roles" : [ 
        "ADMIN"
    ],
    "uuid" : "b8aafdcf-d5c4-4040-a96d-8ab1a8608af8",
    "disabled" : false,
    "company" : {
        "k" : "$id",
        "v" : ObjectId("59a92501df01110fbb6a5dee")
    },
    "company_data" : [ 
        {
            "_id" : ObjectId("59a92501df01110fbb6a5dee"),
            "name" : "Test",
            "gln" : "1234567890123",
            "uuid" : "f1f86961-e8d5-40bb-9d3f-fdbcf549066e",
            "creationDate" : ISODate("2017-09-01T09:14:41.551Z"),
            "lastChange" : ISODate("2017-09-01T09:14:41.551Z"),
            "version" : NumberLong(1),
            "disabled" : false
        }
    ]
}

/* 2 */
{
    "_id" : ObjectId("59a92501df01110fbb6a5df1"),
    "userId" : "sample",
    "userKeyEncrypted" : {
        "salt" : "e3ac48695dea5f51",
        "encryptedAttribute" : "e804758b0fd13c219c3fc383eaa9267b70f7b8a1ed74f05575add713ce11804a"
    },
    "passwordHash" : "$2a$10$Gt2dq1vy4J9MeqDnXjokAOtvFcvbhe/g9wAENXFPaPxLAw1L4EULG",
    "roles" : [ 
        "USER"
    ],
    "uuid" : "55b62d4c-e5ee-408d-80c0-b79e02085b02",
    "disabled" : false,
    "company" : {
        "k" : "$id",
        "v" : ObjectId("59a92501df01110fbb6a5def")
    },
    "company_data" : [ 
        {
            "_id" : ObjectId("59a92501df01110fbb6a5def"),
            "name" : "Sample",
            "gln" : "3210987654321",
            "uuid" : "fee69ee4-b29c-483b-b40d-e702b50b0451",
            "creationDate" : ISODate("2017-09-01T09:14:41.562Z"),
            "lastChange" : ISODate("2017-09-01T09:14:41.562Z"),
            "version" : NumberLong(1),
            "disabled" : false
        }
    ]
}

正如我们希望看到的那样,我们只有包含公司参考的两个用户,并且这两个用户现在在响应中也有完整的公司数据。现在可以应用额外的过滤来摆脱键/值助手并隐藏不需要的数据。

我们提出的最终查询如下所示:

db.user.aggregate([
    { $project: { "userId": 1, "userKeyEncrypted": 1, "uuid":1, "roles": 1, "passwordHash": 1, "disabled": 1, company: { $objectToArray: "$$ROOT.company" }} }, 
    { $unwind: "$company" }, 
    { $match: { "company.k": "$id"}  }, 
    { $lookup: { from: "company", localField: "company.v", foreignField: "_id", as: "company_data" } },
    { $project: { "userId": 1, "userKeyEncrypted": 1, "uuid":1, "roles": 1, "passwordHash": 1, "disabled": 1,  "companyUuid": { $arrayElemAt: [ "$company_data.uuid", 0 ] } } }
])

最终返回我们想要的表示:

/* 1 */
{
    "_id" : ObjectId("59a92501df01110fbb6a5df0"),
    "userId" : "admin",
    "userKeyEncrypted" : {
        "salt" : "78e0528db239fd86",
        "encryptedAttribute" : "e4543ddac7cca9757721379e4e70567bb13956694f473b73f7723ac2e2fc5245"
    },
    "passwordHash" : "$2a$10$STRNORu9rcbq4qYUMld4G.HJk8QQQQBmAswSNC/4PBn2bih0BvjM6",
    "roles" : [ 
        "ADMIN"
    ],
    "uuid" : "b8aafdcf-d5c4-4040-a96d-8ab1a8608af8",
    "disabled" : false,
    "companyUuid" : "f1f86961-e8d5-40bb-9d3f-fdbcf549066e"
}

/* 2 */
{
    "_id" : ObjectId("59a92501df01110fbb6a5df1"),
    "userId" : "sample",
    "userKeyEncrypted" : {
        "salt" : "e3ac48695dea5f51",
        "encryptedAttribute" : "e804758b0fd13c219c3fc383eaa9267b70f7b8a1ed74f05575add713ce11804a"
    },
    "passwordHash" : "$2a$10$Gt2dq1vy4J9MeqDnXjokAOtvFcvbhe/g9wAENXFPaPxLAw1L4EULG",
    "roles" : [ 
        "USER"
    ],
    "uuid" : "55b62d4c-e5ee-408d-80c0-b79e02085b02",
    "disabled" : false,
    "companyUuid" : "fee69ee4-b29c-483b-b40d-e702b50b0451"
}

这种方法的一些最后说明:遗憾的是,这种聚合不是很快,但至少它完成了工作。我还没有按照最初的要求使用一系列引用对其进行测试,尽管这可能需要一些额外的展开。


更新:另一种聚合数据的方式更符合上述错误报告中的评论,如下所示:

db.user.aggregate([
    { $project: { "userId": 1, "userKeyEncrypted": 1, "uuid":1, "roles": 1, "passwordHash": 1, "disabled": 1, companyRefs: { $let: { vars: { refParts: { $objectToArray: "$$ROOT.company" }}, in: "$$refParts.v" } } } },
    { $match: { "companyRefs": { $exists: true } } },
    { $project: { "userId": 1, "userKeyEncrypted": 1, "uuid":1, "roles": 1, "passwordHash": 1, "disabled": 1, "companyRef": { $arrayElemAt: [ "$companyRefs", 1 ] } } },
    { $lookup: { from: "company", localField: "companyRef", foreignField: "_id", as: "company_data" } },
    { $project: { "userId": 1, "userKeyEncrypted": 1, "uuid":1, "roles": 1, "passwordHash": 1, "disabled": 1,  "companyUuid": { $arrayElemAt: [ "$company_data.uuid", 0 ] } } }
])

这里的$let: { vars: ..., in: ... }操作将引用的键和值复制到自己的对象中,从而允许稍后通过相应的操作查找引用。

这些聚合中哪一个表现更好还有待分析。

@Christophe 如果您一直需要查找,NoSQL 数据库可能不适合您的模型,或者您应该重申您想要/需要存储在 NoSQL 集合中的数据模型。对于其他集合中的罕见查找,当前的工具集应该没问题,特别是如果您的应用程序已经严重依赖条目的丢失结构。将所有数据导出到 SQL 只是为了从更好的连接中获利也是矫枉过正
2021-03-18 17:31:52
@Christophe 如果将 SQL 与 NoSQL 数据库进行比较,您可能会很快注意到的一件事是 SQL 数据库通常以第三范式 (3NF) 存储数据,这通常会导致将数据拆分为多个需要引用和连接的表稍后的。相反,在 NoSQL 数据库中,通常所有需要的数据都包含在条目本身中,因此很少需要查找引用,尽管有时可能会出现这样的要求,例如在我的情况下,即 SQL 是否可能是优于 NoSQL 主要取决于您的需求。
2021-03-25 17:31:52
这比使用 SQL 更好吗?
2021-03-31 17:31:52
你好罗曼,对不起,如果我的话伤害了你。这不是故意的。我在 SQL 方面工作了 25 年,现在在 mongo 方面只工作了 1 年,所以是 NoSQL。老实说,我很难尝试在 MongoDB 中执行此类基本请求,有时我想知道我们为什么要这样做。我正在为现有项目工作,因此几乎不可能更改为 SQL。我有 6 个实体相互连接,因此将所有内容都放在一个集合中几乎不可用。无论如何,我找到了另一种方法来检索我想要的数据。
2021-04-10 17:31:52

嗯..你可以查询Bar模型中_id的所有文件的testprop: true,然后做一个查找$in并填充barsFoo模型的数组_id是你第一个查询了..:P

也许这算作“在客户端”:P 只是一个想法。

以前这是不可能的,但是从 Mongo v3.4 的改进我们可以非常接近它。

你可以mongo-join-query. 您的代码如下所示:

const mongoose = require("mongoose");
const joinQuery = require("mongo-join-query");

joinQuery(
    mongoose.models.Foo,
    {
        find: { "bars.testprop": { $in: [true] } },
        populate: ["bars"]
    },
    (err, res) => (err ? console.log("Error:", err) : console.log("Success:", res.results))
);

它是如何工作的?

在后台mongo-join-query将使用您的 Mongoose 模式来确定要加入的模型,并将创建一个聚合管道来执行连接和查询。

披露:我编写了这个库来解决这个用例。