如何在 MongoDB 中执行等效的 SQL Join?
例如,假设您有两个集合(用户和评论),我想提取 pid=444 的所有评论以及每个集合的用户信息。
comments
{ uid:12345, pid:444, comment="blah" }
{ uid:12345, pid:888, comment="asdf" }
{ uid:99999, pid:444, comment="qwer" }
users
{ uid:12345, name:"john" }
{ uid:99999, name:"mia" }
有没有办法一次性提取具有某个字段的所有评论(例如 ...find({pid:444}) )以及与每个评论关联的用户信息?
目前,我首先得到符合我标准的评论,然后找出该结果集中的所有 uid,获取用户对象,并将它们与评论的结果合并。好像我做错了。
从 Mongo 3.2 开始,这个问题的答案大多不再正确。添加到聚合管道的新 $lookup 运算符本质上与左外连接相同:
https://docs.mongodb.org/master/reference/operator/aggregation/lookup/#pipe._S_lookup
从文档:
{
$lookup:
{
from: <collection to join>,
localField: <field from the input documents>,
foreignField: <field from the documents of the "from" collection>,
as: <output array field>
}
}
当然,Mongo 不是关系数据库,开发人员正在谨慎地推荐 $lookup 的特定用例,但至少从 3.2 开始,现在可以使用 MongoDB 进行连接。
我们可以使用 mongodb 客户端控制台在几行中通过简单的功能合并/连接一个集合中的所有数据,现在我们可以执行所需的查询。下面是一个完整的例子,
.- 作者:
db.authors.insert([
{
_id: 'a1',
name: { first: 'orlando', last: 'becerra' },
age: 27
},
{
_id: 'a2',
name: { first: 'mayra', last: 'sanchez' },
age: 21
}
]);
.- 类别:
db.categories.insert([
{
_id: 'c1',
name: 'sci-fi'
},
{
_id: 'c2',
name: 'romance'
}
]);
.- 书籍
db.books.insert([
{
_id: 'b1',
name: 'Groovy Book',
category: 'c1',
authors: ['a1']
},
{
_id: 'b2',
name: 'Java Book',
category: 'c2',
authors: ['a1','a2']
},
]);
.- 图书借阅
db.lendings.insert([
{
_id: 'l1',
book: 'b1',
date: new Date('01/01/11'),
lendingBy: 'jose'
},
{
_id: 'l2',
book: 'b1',
date: new Date('02/02/12'),
lendingBy: 'maria'
}
]);
。- 魔法:
db.books.find().forEach(
function (newBook) {
newBook.category = db.categories.findOne( { "_id": newBook.category } );
newBook.lendings = db.lendings.find( { "book": newBook._id } ).toArray();
newBook.authors = db.authors.find( { "_id": { $in: newBook.authors } } ).toArray();
db.booksReloaded.insert(newBook);
}
);
.- 获取新的集合数据:
db.booksReloaded.find().pretty()
。- 回复 :)
{
"_id" : "b1",
"name" : "Groovy Book",
"category" : {
"_id" : "c1",
"name" : "sci-fi"
},
"authors" : [
{
"_id" : "a1",
"name" : {
"first" : "orlando",
"last" : "becerra"
},
"age" : 27
}
],
"lendings" : [
{
"_id" : "l1",
"book" : "b1",
"date" : ISODate("2011-01-01T00:00:00Z"),
"lendingBy" : "jose"
},
{
"_id" : "l2",
"book" : "b1",
"date" : ISODate("2012-02-02T00:00:00Z"),
"lendingBy" : "maria"
}
]
}
{
"_id" : "b2",
"name" : "Java Book",
"category" : {
"_id" : "c2",
"name" : "romance"
},
"authors" : [
{
"_id" : "a1",
"name" : {
"first" : "orlando",
"last" : "becerra"
},
"age" : 27
},
{
"_id" : "a2",
"name" : {
"first" : "mayra",
"last" : "sanchez"
},
"age" : 21
}
],
"lendings" : [ ]
}
我希望这些线可以帮助你。
官方 mongodb 网站上的这个页面正好解决了这个问题:
当我们显示我们的故事列表时,我们需要显示发布故事的用户的姓名。如果我们使用关系数据库,我们可以对用户和存储执行连接,并在单个查询中获取所有对象。但是 MongoDB 不支持连接,因此有时需要一些非规范化。在这里,这意味着缓存“用户名”属性。关系纯粹主义者可能已经感到不安,就好像我们违反了一些普遍规律一样。但请记住,MongoDB 集合并不等同于关系表;每个都有一个独特的设计目标。规范化表提供了一个原子的、隔离的数据块。然而,一个文档更接近地代表了一个整体的对象。在社交新闻网站的情况下,可以说用户名是发布的故事所固有的。
您必须按照您描述的方式进行操作。 MongoDB 是一个非关系型数据库,不支持连接。
通过 $lookup、$project 和 $match 的正确组合,您可以在多个参数上连接多个表。这是因为它们可以被链接多次。
假设我们要执行以下操作 (reference)
SELECT S.* FROM LeftTable S
LEFT JOIN RightTable R ON S.ID = R.ID AND S.MID = R.MID
WHERE R.TIM > 0 AND S.MOB IS NOT NULL
第 1 步:链接所有表
您可以根据需要 $lookup 任意数量的表。
$lookup - 查询中的每个表一个
$unwind - 正确反规范化 data ,否则它会被包裹在数组中
Python代码..
db.LeftTable.aggregate([
# connect all tables
{"$lookup": {
"from": "RightTable",
"localField": "ID",
"foreignField": "ID",
"as": "R"
}},
{"$unwind": "R"}
])
第 2 步:定义所有条件
$project :在此处定义所有条件语句,以及您要选择的所有变量。
Python代码..
db.LeftTable.aggregate([
# connect all tables
{"$lookup": {
"from": "RightTable",
"localField": "ID",
"foreignField": "ID",
"as": "R"
}},
{"$unwind": "R"},
# define conditionals + variables
{"$project": {
"midEq": {"$eq": ["$MID", "$R.MID"]},
"ID": 1, "MOB": 1, "MID": 1
}}
])
第 3 步:加入所有条件
$match - 使用 OR 或 AND 等连接所有条件。这些条件可以有多个。
$project:取消定义所有条件
完整的 Python 代码..
db.LeftTable.aggregate([
# connect all tables
{"$lookup": {
"from": "RightTable",
"localField": "ID",
"foreignField": "ID",
"as": "R"
}},
{"$unwind": "$R"},
# define conditionals + variables
{"$project": {
"midEq": {"$eq": ["$MID", "$R.MID"]},
"ID": 1, "MOB": 1, "MID": 1
}},
# join all conditionals
{"$match": {
"$and": [
{"R.TIM": {"$gt": 0}},
{"MOB": {"$exists": True}},
{"midEq": {"$eq": True}}
]}},
# undefine conditionals
{"$project": {
"midEq": 0
}}
])
几乎任何表、条件和连接的组合都可以以这种方式完成。
{"$unwind ":"R"}
一个错误,如果它改为 {"$unwind":"$R"}
,它就完美了!
正如其他人指出的那样,您正在尝试从您真的不想做的非关系数据库创建一个关系数据库,但无论如何,如果您有一个必须这样做的案例,那么您可以使用一个解决方案。我们首先对集合 A(或在您的情况下为用户)进行 foreach 查找,然后我们将每个项目作为一个对象,然后我们使用对象属性(在您的情况下为 uid)在我们的第二个集合中查找(在您的情况下为评论),如果我们可以找到它然后我们有一个匹配,我们可以打印或用它做一些事情。希望这对你有帮助,祝你好运:)
db.users.find().forEach(
function (object) {
var commonInBoth=db.comments.findOne({ "uid": object.uid} );
if (commonInBoth != null) {
printjson(commonInBoth) ;
printjson(object) ;
}else {
// did not match so we don't care in this case
}
});
您可以使用 3.2 版本中提供的查找来加入 Mongo 中的两个集合。在您的情况下,查询将是
db.comments.aggregate({
$lookup:{
from:"users",
localField:"uid",
foreignField:"uid",
as:"users_comments"
}
})
或者您也可以加入用户,然后会有一些变化,如下所示。
db.users.aggregate({
$lookup:{
from:"comments",
localField:"uid",
foreignField:"uid",
as:"users_comments"
}
})
它将与 SQL 中的左右连接一样工作。
这是“加入” * Actors 和 Movies 集合的示例:
https://github.com/mongodb/cookbook/blob/master/content/patterns/pivot.txt
它利用 .mapReduce()
方法
join - 加入面向文档的数据库的替代方法
$lookup(聚合)
对同一数据库中的未分片集合执行左外连接,以过滤来自“已连接”集合的文档以进行处理。对于每个输入文档,$lookup 阶段添加一个新的数组字段,其元素是“加入”集合中的匹配文档。 $lookup 阶段将这些重新调整的文档传递到下一个阶段。 $lookup 阶段具有以下语法:
平等匹配
要在输入文档中的字段与“已加入”集合的文档中的字段之间执行相等匹配,$lookup 阶段具有以下语法:
{
$lookup:
{
from: <collection to join>,
localField: <field from the input documents>,
foreignField: <field from the documents of the "from" collection>,
as: <output array field>
}
}
该操作将对应于以下伪 SQL 语句:
SELECT *, <output array field>
FROM collection
WHERE <output array field> IN (SELECT <documents as determined from the pipeline>
FROM <collection to join>
WHERE <pipeline> );
这取决于你想要做什么。
您当前已将其设置为规范化数据库,这很好,并且您执行此操作的方式是合适的。
但是,还有其他方法可以做到这一点。
您可以拥有一个帖子集合,其中嵌入了每个帖子的评论,其中包含对您可以迭代查询以获取的用户的引用。您可以将用户名与评论一起存储,您可以将它们全部存储在一个文档中。
NoSQL 的特点是它专为灵活的模式和非常快速的读写而设计。在典型的大数据场中,数据库是最大的瓶颈,数据库引擎比应用程序和前端服务器少……它们更昂贵但更强大,硬盘空间也相对便宜。规范化来自试图节省空间的概念,但它带来了使您的数据库执行复杂的联接和验证关系的完整性、执行级联操作的成本。如果他们正确设计数据库,所有这些都可以为开发人员节省一些麻烦。
使用 NoSQL,如果您接受冗余和存储空间不是问题,因为它们的成本(更新所需的处理器时间和存储额外数据的硬盘成本),非规范化不是问题(对于成为数十万个项目可能是性能问题,但大多数时候这不是问题)。此外,每个数据库集群都有多个应用程序和前端服务器。让他们完成连接的繁重工作,让数据库服务器坚持读写。
TL;DR:你正在做的很好,还有其他方法可以做到。查看 mongodb 文档的数据模型模式以获取一些很好的示例。 http://docs.mongodb.org/manual/data-modeling/
许多驱动程序都支持一种称为 DBRef 的规范。
DBRef 是用于在文档之间创建引用的更正式的规范。 DBRefs(通常)包括一个集合名称和一个对象 ID。大多数开发人员仅在集合可以从一个文档更改为下一个文档时才使用 DBRefs。如果您引用的集合始终相同,则上述手动引用会更有效。
取自 MongoDB 文档:Data Models >数据模型参考 > Database References
3.2.6之前,mongodb不像mysql那样支持join查询。以下解决方案适合您。
db.getCollection('comments').aggregate([
{$match : {pid : 444}},
{$lookup: {from: "users",localField: "uid",foreignField: "uid",as: "userData"}},
])
MongoDB 不允许连接,但您可以使用插件来处理它。检查 mongo-join 插件。这是最好的,我已经用过了。您可以像 npm install mongo-join
一样直接使用 npm 安装它。您可以查看 full documentation with examples。
(++) 当我们需要加入 (N) 个集合时非常有用的工具
(--) 我们可以在查询的顶层应用条件
例子
var Join = require('mongo-join').Join, mongodb = require('mongodb'), Db = mongodb.Db, Server = mongodb.Server;
db.open(function (err, Database) {
Database.collection('Appoint', function (err, Appoints) {
/* we can put conditions just on the top level */
Appoints.find({_id_Doctor: id_doctor ,full_date :{ $gte: start_date },
full_date :{ $lte: end_date }}, function (err, cursor) {
var join = new Join(Database).on({
field: '_id_Doctor', // <- field in Appoints document
to: '_id', // <- field in User doc. treated as ObjectID automatically.
from: 'User' // <- collection name for User doc
}).on({
field: '_id_Patient', // <- field in Appoints doc
to: '_id', // <- field in User doc. treated as ObjectID automatically.
from: 'User' // <- collection name for User doc
})
join.toArray(cursor, function (err, joinedDocs) {
/* do what ever you want here */
/* you can fetch the table and apply your own conditions */
.....
.....
.....
resp.status(200);
resp.json({
"status": 200,
"message": "success",
"Appoints_Range": joinedDocs,
});
return resp;
});
});
您可以使用聚合管道来完成它,但是自己编写它很痛苦。
您可以使用 mongo-join-query
从您的查询中自动创建聚合管道。
这就是您的查询的样子:
const mongoose = require("mongoose");
const joinQuery = require("mongo-join-query");
joinQuery(
mongoose.models.Comment,
{
find: { pid:444 },
populate: ["uid"]
},
(err, res) => (err ? console.log("Error:", err) : console.log("Success:", res.results))
);
您的结果将在 uid
字段中包含用户对象,您可以根据需要链接任意多个级别。您可以填充对用户的引用,它引用了一个团队,它引用了其他东西,等等。
免责声明:我写了 mongo-join-query
来解决这个确切的问题。
playORM 可以使用 S-SQL(可扩展 SQL)为您完成此操作,它只是添加了分区,以便您可以在分区内进行连接。
不,看起来你做错了。 MongoDB 连接是“客户端”。和你说的差不多:
目前,我首先得到符合我标准的评论,然后找出该结果集中的所有 uid,获取用户对象,并将它们与评论的结果合并。好像我做错了。
1) Select from the collection you're interested in.
2) From that collection pull out ID's you need
3) Select from other collections
4) Decorate your original results.
这不是一个“真正的”连接,但它实际上比 SQL 连接更有用,因为您不必处理“多”边连接的重复行,而是装饰最初选择的集合。
此页面上有很多废话和 FUD。事实证明 5 年后 MongoDB 仍然是一件事。
我认为,如果您需要规范化的数据表 - 您需要尝试其他一些数据库解决方案。
但我在 Git 上找到了 MOngo 的解决方案顺便说一句,在插入代码中 - 它有电影的名称,但 noi 电影的 ID。
问题
您有一组 Actors 以及他们制作的一系列电影。
您想要生成一个包含一组 Actors 的 Movies 集合。
一些样本数据
db.actors.insert( { actor: "Richard Gere", movies: ['Pretty Woman', 'Runaway Bride', 'Chicago'] });
db.actors.insert( { actor: "Julia Roberts", movies: ['Pretty Woman', 'Runaway Bride', 'Erin Brockovich'] });
解决方案
我们需要遍历 Actor 文档中的每部电影并单独发出每部电影。
这里的问题是在减少阶段。我们不能从 reduce 阶段发出一个数组,所以我们必须在返回的“值”文档中构建一个 Actors 数组。
map = function() {
for(var i in this.movies){
key = { movie: this.movies[i] };
value = { actors: [ this.actor ] };
emit(key, value);
}
}
reduce = function(key, values) {
actor_list = { actors: [] };
for(var i in values) {
actor_list.actors = values[i].actors.concat(actor_list.actors);
}
return actor_list;
}
注意actor_list 实际上是一个包含数组的javascript 对象。还要注意 map 发出相同的结构。
运行以下命令执行 map/reduce,将其输出到“pivot”集合并打印结果:
printjson(db.actors.mapReduce(map, reduce, "pivot")); db.pivot.find().forEach(printjson);
这是示例输出,请注意“Pretty Woman”和“Runaway Bride”都有“Richard Gere”和“Julia Roberts”。
{ "_id" : { "movie" : "Chicago" }, "value" : { "actors" : [ "Richard Gere" ] } }
{ "_id" : { "movie" : "Erin Brockovich" }, "value" : { "actors" : [ "Julia Roberts" ] } }
{ "_id" : { "movie" : "Pretty Woman" }, "value" : { "actors" : [ "Richard Gere", "Julia Roberts" ] } }
{ "_id" : { "movie" : "Runaway Bride" }, "value" : { "actors" : [ "Richard Gere", "Julia Roberts" ] } }
我们可以使用 mongoDB 子查询合并两个集合。这是示例,评论--
`db.commentss.insert([
{ uid:12345, pid:444, comment:"blah" },
{ uid:12345, pid:888, comment:"asdf" },
{ uid:99999, pid:444, comment:"qwer" }])`
用户——
db.userss.insert([
{ uid:12345, name:"john" },
{ uid:99999, name:"mia" }])
用于 JOIN 的 MongoDB 子查询——
`db.commentss.find().forEach(
function (newComments) {
newComments.userss = db.userss.find( { "uid": newComments.uid } ).toArray();
db.newCommentUsers.insert(newComments);
}
);`
从新生成的 Collection 中获取结果——
db.newCommentUsers.find().pretty()
结果 -
`{
"_id" : ObjectId("5511236e29709afa03f226ef"),
"uid" : 12345,
"pid" : 444,
"comment" : "blah",
"userss" : [
{
"_id" : ObjectId("5511238129709afa03f226f2"),
"uid" : 12345,
"name" : "john"
}
]
}
{
"_id" : ObjectId("5511236e29709afa03f226f0"),
"uid" : 12345,
"pid" : 888,
"comment" : "asdf",
"userss" : [
{
"_id" : ObjectId("5511238129709afa03f226f2"),
"uid" : 12345,
"name" : "john"
}
]
}
{
"_id" : ObjectId("5511236e29709afa03f226f1"),
"uid" : 99999,
"pid" : 444,
"comment" : "qwer",
"userss" : [
{
"_id" : ObjectId("5511238129709afa03f226f3"),
"uid" : 99999,
"name" : "mia"
}
]
}`
希望这会有所帮助。