问题描述
总的来说,我对 Mongoose 和 MongoDB 还是很陌生,所以我很难确定是否有可能发生这样的事情:
Item = new Schema({id: Schema.ObjectId,dateCreated: { type: Date, default: Date.now },title: { type: String, default: 'No Title' },描述:{ 类型:字符串,默认值:'无描述'},标签: [ { 类型: Schema.ObjectId, ref: 'ItemTag' }]});ItemTag = 新架构({id: Schema.ObjectId,tagId: { type: Schema.ObjectId, ref: 'Tag' },标签名称:{ 类型:字符串}});var query = Models.Item.find({});询问.desc('创建日期').populate('标签').where('tags.tagName').in(['funny', 'politics']).run(function(err, docs){//文档总是空的});
有没有更好的方法来做到这一点?
编辑
对于任何混淆,我们深表歉意.我想要做的是获取所有包含有趣标签或政治标签的项目.
编辑
没有 where 子句的文档:
[{_id:4fe90264e5caa33f04000012,不喜欢:0,喜欢:0,来源:'/uploads/loldog.jpg',注释: [],标签: [{项目编号:4fe90264e5caa33f04000012,tagName: '电影',tagId: 4fe64219007e20e644000007,_id:4fe90270e5caa33f04000015,创建日期:2012 年 6 月 26 日,星期二 00:29:36 GMT,评分:0,不喜欢:0,喜欢:0},{项目编号:4fe90264e5caa33f04000012,标签名称:'有趣',tagId: 4fe64219007e20e644000002,_id:4fe90270e5caa33f04000017,创建日期:2012 年 6 月 26 日,星期二 00:29:36 GMT,评分:0,不喜欢:0,喜欢:0}],观看次数:0,评分:0,类型:'图像',描述:空,title: 'dogggg',创建日期:2012 年 6 月 26 日,星期二 00:29:24 GMT}, ... ]
使用 where 子句,我得到一个空数组.
使用大于 3.2 的现代 MongoDB,您可以使用 $lookup
作为 .populate()
的替代.这也具有实际在服务器上"进行连接的优点,而不是 .populate()
实际上是 多个查询"以模拟"加入.
所以 .populate()
是不是真正意义上的连接",就关系数据库的作用而言.$lookup
运算符在另一个手,实际上在服务器上完成工作,或多或少类似于LEFT JOIN":
Item.aggregate([{$查找":{来自":ItemTags.collection.name,"localField": "标签","foreignField": "_id",作为":标签"}},{ "$unwind": "$tags" },{ "$match": { "tags.tagName": { "$in": [ "funny", "politics" ] } } },{$组":{"_id": "$_id","dateCreated": { "$first": "$dateCreated" },"title": { "$first": "$title" },"description": { "$first": "$description" },标签":{$push":$tags"}}}],功能(错误,结果){//标签"现在按条件过滤并加入"})
注意 这里的 .collection.name
实际上计算的是字符串",它是分配给模型的 MongoDB 集合的实际名称.由于猫鼬默认复数化"集合名称,并且 $lookup
需要实际的 MongoDB 集合名称作为参数(因为它是服务器操作),那么这是在 mongoose 代码中使用的一个方便的技巧,而不是直接硬编码"集合名称.
虽然我们也可以使用 $filter
在数组上删除不需要的项目,这实际上是最有效的形式,因为 聚合管道优化,用于特殊条件 as $lookup
后跟 $unwind
和 $match 条件.
这实际上导致三个流水线阶段合并为一个:
{ "$lookup" : {"from" : "itemtags","as" : "标签",localField":标签","foreignField": "_id",展开":{preserveNullAndEmptyArrays":假},匹配":{标签名称" : {"$in" : [有趣的",政治"]}}}}
这是高度优化的,因为实际操作首先过滤要加入的集合",然后返回结果并展开"数组.两种方法都被采用,所以结果没有打破 16MB 的 BSON 限制,这是客户端没有的约束.
唯一的问题是它在某些方面似乎违反直觉",特别是当您想要数组中的结果时,但这就是 $group
用于这里,因为它重构为原始文档形式.
同样遗憾的是,我们此时根本无法真正编写$lookup
与服务器使用的最终语法相同.恕我直言,这是一个需要纠正的疏忽.但就目前而言,简单地使用该序列是可行的,并且是具有最佳性能和可扩展性的最可行选项.
附录 - MongoDB 3.6 及更高版本
尽管此处显示的模式相当优化,这是由于其他阶段如何进入$lookup
,它确实有一个失败,即 $lookup
和 populate()
被最佳"用法否定code>$unwind 此处不保留空数组.您可以添加 preserveNullAndEmptyArrays
选项,但这会否定上述 优化" 序列,并且基本上保持所有三个阶段完好无损,这通常会在优化中组合.
MongoDB 3.6 以更具表现力的形式进行扩展noreferrer">$lookup
允许子管道"表达式.这不仅满足了保留LEFT JOIN"的目标,而且仍然允许优化查询以减少返回的结果并使用更简化的语法:
Item.aggregate([{$查找":{来自":ItemTags.collection.name,"let": { "tags": "$tags" },管道":[{$匹配":{"tags": { "$in": [ "politics", "funny" ] },"$expr": { "$in": [ "$_id", "$$tags" ] }}}]}}])
使用的 $expr
为了将声明的本地"值与外部"值匹配实际上是 MongoDB 现在在内部"使用原始 $lookup
语法.通过以这种形式表达我们可以定制初始的$match
自己在子管道"中的表达.
事实上,作为一个真正的聚合管道",您可以在这个子管道"表达式中对聚合管道做任何可以做的事情,包括嵌套"$lookup
到其他相关集合.
进一步的使用有点超出这里问题的范围,但即使是嵌套人口",那么 $lookup
允许这大致相同,并且 lot" 更强大在充分利用.
工作示例
下面给出了一个在模型上使用静态方法的例子.一旦实现了该静态方法,调用就变成了:
Item.lookup({路径:'标签',查询:{ 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }},打回来)
或者增强一点现代感甚至变成:
let results = await Item.lookup({路径:'标签',查询:{ 'tagName' : { '$in': [ 'funny', 'politics' ] } }})
使它在结构上与 .populate()
非常相似,但它实际上是在服务器上进行连接.为了完整起见,这里的用法是根据父子情况将返回的数据转换回 mongoose 文档实例.
它相当琐碎且易于适应或仅适用于大多数常见情况.
注意这里使用 async 只是为了运行所附示例的简洁性.实际实现没有这种依赖性.
const async = require('async'),猫鼬 = 要求('猫鼬'),架构 = 猫鼬.架构;猫鼬.Promise = global.Promise;mongoose.set('debug', true);mongoose.connect('mongodb://localhost/looktest');const itemTagSchema = 新架构({标签名称:字符串});const itemSchema = 新架构({dateCreated: { type: Date, default: Date.now },标题:字符串,描述:字符串,标签:[{ 类型:Schema.Types.ObjectId,参考:'ItemTag'}]});itemSchema.statics.lookup = 函数(选择,回调){让 rel =mongoose.model(this.schema.path(opt.path).caster.options.ref);让组 = { "$group": { } };this.schema.eachPath(p =>group.$group[p] = (p === "_id") ?$_id":(p === opt.path) ?{ "$push": `$${p}` } : { "$first": `$${p}` });让管道 = [{$查找":{来自":rel.collection.name,作为":opt.path,localField":opt.path,"foreignField": "_id"}},{ "$unwind": `$${opt.path}` },{ "$match": opt.query },团体];this.aggregate(pipeline,(err,result) => {如果(错误)回调(错误);结果 = result.map(m => {m[opt.path] = m[opt.path].map(r => rel(r));返回这个(m);});回调(错误,结果);});}const Item = mongoose.model('Item', itemSchema);const ItemTag = mongoose.model('ItemTag', itemTagSchema);功能日志(主体){console.log(JSON.stringify(body, undefined, 2))}异步系列([//清理数据(回调) =>async.each(mongoose.models,(model,callback) =>model.remove({},callback),callback),//创建标签和项目(回调) =>异步瀑布([(回调) =>ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],打回来),(标签,回调)=>Item.create({ "title": "Something","description": "一个项目",标签":标签},回调)],打回来),//使用我们的静态查询(回调) =>项目.查找({路径:'标签',查询:{ 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }},打回来)],(错误,结果)=>{如果(错误)抛出错误;让结果 = results.pop();日志(结果);猫鼬.disconnect();})
或者使用 async/await
对 Node 8.x 及更高版本更现代,并且没有额外的依赖:
const { Schema } = mongoose = require('mongoose');const uri = 'mongodb://localhost/looktest';猫鼬.Promise = global.Promise;mongoose.set('debug', true);const itemTagSchema = 新架构({标签名称:字符串});const itemSchema = 新架构({dateCreated: { type: Date, default: Date.now },标题:字符串,描述:字符串,标签:[{ 类型:Schema.Types.ObjectId,参考:'ItemTag'}]});itemSchema.statics.lookup = 函数(选择){让 rel =mongoose.model(this.schema.path(opt.path).caster.options.ref);让组 = { "$group": { } };this.schema.eachPath(p =>group.$group[p] = (p === "_id") ?$_id":(p === opt.path) ?{ "$push": `$${p}` } : { "$first": `$${p}` });让管道 = [{$查找":{来自":rel.collection.name,作为":opt.path,localField":opt.path,"foreignField": "_id"}},{ "$unwind": `$${opt.path}` },{ "$match": opt.query },团体];返回 this.aggregate(pipeline).exec().then(r => r.map(m =>)this({ ...m, [opt.path]: m[opt.path].map(r => rel(r)) })));}const Item = mongoose.model('Item', itemSchema);const ItemTag = mongoose.model('ItemTag', itemTagSchema);const log = body =>console.log(JSON.stringify(body, undefined, 2));(异步函数(){尝试 {const conn = await mongoose.connect(uri);//清理数据await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));//创建标签和项目const 标签 = 等待 ItemTag.create([电影",有趣"].map(tagName =>({ tagName })));const item = await Item.create({"title": "东西","description": "一个项目",标签});//使用我们的静态查询const 结果 = (等待 Item.lookup({路径:'标签',查询:{ 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }})).流行音乐();日志(结果);猫鼬.disconnect();}赶上(e){控制台错误(e);} 最后 {进程.退出()}})()
从 MongoDB 3.6 开始,即使没有 $unwind
和 $group
建筑:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');const uri = 'mongodb://localhost/looktest';猫鼬.Promise = global.Promise;mongoose.set('debug', true);const itemTagSchema = 新架构({标签名称:字符串});const itemSchema = 新架构({标题:字符串,描述:字符串,标签:[{ 类型:Schema.Types.ObjectId,参考:'ItemTag'}]},{ 时间戳:真 });itemSchema.statics.lookup = function({ path, query }) {让 rel =mongoose.model(this.schema.path(path).caster.options.ref);//MongoDB 3.6 及更高版本 $lookup with sub-pipeline让管道 = [{$查找":{来自":rel.collection.name,作为":路径,"let": { [path]: `$${path}` },管道":[{$匹配":{...询问,"$expr": { "$in": [ "$_id", `$$${path}` ] }}}]}}];返回 this.aggregate(pipeline).exec().then(r => r.map(m =>)this({ ...m, [path]: m[path].map(r => rel(r)) })));};const Item = mongoose.model('Item', itemSchema);const ItemTag = mongoose.model('ItemTag', itemTagSchema);const log = body =>console.log(JSON.stringify(body, undefined, 2));(异步函数(){尝试 {const conn = await mongoose.connect(uri);//清理数据await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));//创建标签和项目const 标签 = 等待 ItemTag.insertMany([电影",有趣"].map(tagName => ({ tagName })));const item = await Item.create({"title": "东西","description": "一个项目",标签});//使用我们的静态查询让结果 = (等待 Item.lookup({路径:'标签',查询:{ 'tagName': { '$in': [ 'funny', 'politics' ] } }})).流行音乐();日志(结果);等待 mongoose.disconnect();}赶上(e){控制台错误(e)} 最后 {进程.退出()}})()
I'm pretty new to Mongoose and MongoDB in general so I'm having a difficult time figuring out if something like this is possible:
Item = new Schema({
id: Schema.ObjectId,
dateCreated: { type: Date, default: Date.now },
title: { type: String, default: 'No Title' },
description: { type: String, default: 'No Description' },
tags: [ { type: Schema.ObjectId, ref: 'ItemTag' }]
});
ItemTag = new Schema({
id: Schema.ObjectId,
tagId: { type: Schema.ObjectId, ref: 'Tag' },
tagName: { type: String }
});
var query = Models.Item.find({});
query
.desc('dateCreated')
.populate('tags')
.where('tags.tagName').in(['funny', 'politics'])
.run(function(err, docs){
// docs is always empty
});
Is there a better way do this?
Edit
Apologies for any confusion. What I'm trying to do is get all Items that contain either the funny tag or politics tag.
Edit
Document without where clause:
[{
_id: 4fe90264e5caa33f04000012,
dislikes: 0,
likes: 0,
source: '/uploads/loldog.jpg',
comments: [],
tags: [{
itemId: 4fe90264e5caa33f04000012,
tagName: 'movies',
tagId: 4fe64219007e20e644000007,
_id: 4fe90270e5caa33f04000015,
dateCreated: Tue, 26 Jun 2012 00:29:36 GMT,
rating: 0,
dislikes: 0,
likes: 0
},
{
itemId: 4fe90264e5caa33f04000012,
tagName: 'funny',
tagId: 4fe64219007e20e644000002,
_id: 4fe90270e5caa33f04000017,
dateCreated: Tue, 26 Jun 2012 00:29:36 GMT,
rating: 0,
dislikes: 0,
likes: 0
}],
viewCount: 0,
rating: 0,
type: 'image',
description: null,
title: 'dogggg',
dateCreated: Tue, 26 Jun 2012 00:29:24 GMT
}, ... ]
With the where clause, I get an empty array.
With a modern MongoDB greater than 3.2 you can use $lookup
as an alternate to .populate()
in most cases. This also has the advantage of actually doing the join "on the server" as opposed to what .populate()
does which is actually "multiple queries" to "emulate" a join.
So .populate()
is not really a "join" in the sense of how a relational database does it. The $lookup
operator on the other hand, actually does the work on the server, and is more or less analogous to a "LEFT JOIN":
Item.aggregate(
[
{ "$lookup": {
"from": ItemTags.collection.name,
"localField": "tags",
"foreignField": "_id",
"as": "tags"
}},
{ "$unwind": "$tags" },
{ "$match": { "tags.tagName": { "$in": [ "funny", "politics" ] } } },
{ "$group": {
"_id": "$_id",
"dateCreated": { "$first": "$dateCreated" },
"title": { "$first": "$title" },
"description": { "$first": "$description" },
"tags": { "$push": "$tags" }
}}
],
function(err, result) {
// "tags" is now filtered by condition and "joined"
}
)
Whilst we could also use $filter
on arrays to remove the unwanted items, this is actually the most efficient form due to Aggregation Pipeline Optimization for the special condition of as $lookup
followed by both an $unwind
and a $match
condition.
This actually results in the three pipeline stages being rolled into one:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
This is highly optimal as the actual operation "filters the collection to join first", then it returns the results and "unwinds" the array. Both methods are employed so the results do not break the BSON limit of 16MB, which is a constraint that the client does not have.
The only problem is that it seems "counter-intuitive" in some ways, particularly when you want the results in an array, but that is what the $group
is for here, as it reconstructs to the original document form.
It's also unfortunate that we simply cannot at this time actually write $lookup
in the same eventual syntax the server uses. IMHO, this is an oversight to be corrected. But for now, simply using the sequence will work and is the most viable option with the best performance and scalability.
Addendum - MongoDB 3.6 and upwards
Though the pattern shown here is fairly optimized due to how the other stages get rolled into the $lookup
, it does have one failing in that the "LEFT JOIN" which is normally inherent to both $lookup
and the actions of populate()
is negated by the "optimal" usage of $unwind
here which does not preserve empty arrays. You can add the preserveNullAndEmptyArrays
option, but this negates the "optimized" sequence described above and essentially leaves all three stages intact which would normally be combined in the optimization.
MongoDB 3.6 expands with a "more expressive" form of $lookup
allowing a "sub-pipeline" expression. Which not only meets the goal of retaining the "LEFT JOIN" but still allows an optimal query to reduce results returned and with a much simplified syntax:
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
The $expr
used in order to match the declared "local" value with the "foreign" value is actually what MongoDB does "internally" now with the original $lookup
syntax. By expressing in this form we can tailor the initial $match
expression within the "sub-pipeline" ourselves.
In fact, as a true "aggregation pipeline" you can do just about anything you can do with an aggregation pipeline within this "sub-pipeline" expression, including "nesting" the levels of $lookup
to other related collections.
Further usage is a bit beyond the scope of what the question here asks, but in relation to even "nested population" then the new usage pattern of $lookup
allows this to be much the same, and a "lot" more powerful in it's full usage.
Working Example
The following gives an example using a static method on the model. Once that static method is implemented the call simply becomes:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Or enhancing to be a bit more modern even becomes:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Making it very similar to .populate()
in structure, but it's actually doing the join on the server instead. For completeness, the usage here casts the returned data back to mongoose document instances at according to both the parent and child cases.
It's fairly trivial and easy to adapt or just use as is for most common cases.
const async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.connect('mongodb://localhost/looktest');
const itemTagSchema = new Schema({
tagName: String
});
const itemSchema = new Schema({
dateCreated: { type: Date, default: Date.now },
title: String,
description: String,
tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});
itemSchema.statics.lookup = function(opt,callback) {
let rel =
mongoose.model(this.schema.path(opt.path).caster.options.ref);
let group = { "$group": { } };
this.schema.eachPath(p =>
group.$group[p] = (p === "_id") ? "$_id" :
(p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });
let pipeline = [
{ "$lookup": {
"from": rel.collection.name,
"as": opt.path,
"localField": opt.path,
"foreignField": "_id"
}},
{ "$unwind": `$${opt.path}` },
{ "$match": opt.query },
group
];
this.aggregate(pipeline,(err,result) => {
if (err) callback(err);
result = result.map(m => {
m[opt.path] = m[opt.path].map(r => rel(r));
return this(m);
});
callback(err,result);
});
}
const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);
function log(body) {
console.log(JSON.stringify(body, undefined, 2))
}
async.series(
[
// Clean data
(callback) => async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
// Create tags and items
(callback) =>
async.waterfall(
[
(callback) =>
ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],
callback),
(tags, callback) =>
Item.create({ "title": "Something","description": "An item",
"tags": tags },callback)
],
callback
),
// Query with our static
(callback) =>
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
],
(err,results) => {
if (err) throw err;
let result = results.pop();
log(result);
mongoose.disconnect();
}
)
Or a little more modern for Node 8.x and above with async/await
and no additional dependencies:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/looktest';
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
const itemTagSchema = new Schema({
tagName: String
});
const itemSchema = new Schema({
dateCreated: { type: Date, default: Date.now },
title: String,
description: String,
tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});
itemSchema.statics.lookup = function(opt) {
let rel =
mongoose.model(this.schema.path(opt.path).caster.options.ref);
let group = { "$group": { } };
this.schema.eachPath(p =>
group.$group[p] = (p === "_id") ? "$_id" :
(p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });
let pipeline = [
{ "$lookup": {
"from": rel.collection.name,
"as": opt.path,
"localField": opt.path,
"foreignField": "_id"
}},
{ "$unwind": `$${opt.path}` },
{ "$match": opt.query },
group
];
return this.aggregate(pipeline).exec().then(r => r.map(m =>
this({ ...m, [opt.path]: m[opt.path].map(r => rel(r)) })
));
}
const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);
const log = body => console.log(JSON.stringify(body, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
// Clean data
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
// Create tags and items
const tags = await ItemTag.create(
["movies", "funny"].map(tagName =>({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
// Query with our static
const result = (await Item.lookup({
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
})).pop();
log(result);
mongoose.disconnect();
} catch (e) {
console.error(e);
} finally {
process.exit()
}
})()
And from MongoDB 3.6 and upward, even without the $unwind
and $group
building:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/looktest';
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
const itemTagSchema = new Schema({
tagName: String
});
const itemSchema = new Schema({
title: String,
description: String,
tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
},{ timestamps: true });
itemSchema.statics.lookup = function({ path, query }) {
let rel =
mongoose.model(this.schema.path(path).caster.options.ref);
// MongoDB 3.6 and up $lookup with sub-pipeline
let pipeline = [
{ "$lookup": {
"from": rel.collection.name,
"as": path,
"let": { [path]: `$${path}` },
"pipeline": [
{ "$match": {
...query,
"$expr": { "$in": [ "$_id", `$$${path}` ] }
}}
]
}}
];
return this.aggregate(pipeline).exec().then(r => r.map(m =>
this({ ...m, [path]: m[path].map(r => rel(r)) })
));
};
const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);
const log = body => console.log(JSON.stringify(body, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
// Clean data
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
// Create tags and items
const tags = await ItemTag.insertMany(
["movies", "funny"].map(tagName => ({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
// Query with our static
let result = (await Item.lookup({
path: 'tags',
query: { 'tagName': { '$in': [ 'funny', 'politics' ] } }
})).pop();
log(result);
await mongoose.disconnect();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
这篇关于在 Mongoose 中填充后查询的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!