一般而言,我对Mongoose和MongoDB还是很陌生,所以我很难确定是否可以进行以下操作:
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 });
有更好的方法吗?
编辑
如有任何混淆,我们深表歉意。我想做的是获取所有包含有趣标签或政治标签的商品。
没有where子句的文档:
[{ _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 }, ... ]
使用where子句,我得到一个空数组。
对于大于3.2的现代MongoDB,您可以在大多数情况下$lookup用作替代.populate()。这也有实际上做加盟,而不是什么“在服务器上”的优势.populate(),实际上是做 “多次查询”,以“模仿” 的联接。
$lookup
.populate()
所以,.populate()是 不是 真的在关系数据库中是如何做的意义上的“加盟”。在$lookup另一方面,运营商,实际执行服务器上的工作,是一个或多或少类似 “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" } )
注意 :.collection.name这里实际上计算的是“字符串”,它是分配给模型的MongoDB集合的实际名称。由于猫鼬默认情况下会“复数化”集合名称,并且$lookup需要实际的MongoDB集合名称作为参数(因为这是服务器操作),因此这是在猫鼬代码中使用的便捷技巧,而不是直接对集合名称进行“硬编码” 。
.collection.name
虽然我们也可以$filter在数组上使用以删除不需要的项目,但实际上这是最有效的形式,这是由于针对条件as 和条件an 的特殊条件进行了聚合管道优化。$lookup$unwind$match
$filter
$unwind
$match
实际上,这导致三个管道阶段被分解为一个阶段:
{ "$lookup" : { "from" : "itemtags", "as" : "tags", "localField" : "tags", "foreignField" : "_id", "unwinding" : { "preserveNullAndEmptyArrays" : false }, "matching" : { "tagName" : { "$in" : [ "funny", "politics" ] } } }}
这是最佳选择,因为实际操作“先过滤要加入的集合”,然后返回结果并“展开”数组。两种方法都被采用,因此结果不会超过16MB的BSON限制,这是客户端没有的限制。
唯一的问题是,在某些方面它似乎是“反直觉的”,尤其是当您希望将结果存储在数组中时,但这$group就是这里的意义,因为它可以重构为原始文档格式。
$group
不幸的是,我们此时根本无法$lookup使用服务器使用的相同最终语法进行编写。恕我直言,这是一个需要纠正的疏忽。但是就目前而言,简单地使用序列即可,并且是具有最佳性能和可伸缩性的最可行选择。
尽管此处显示的模式由于其他阶段如何进入而已进行了 相当优化$lookup,但它确实存在一个失败之处,即这通常是两者固有的“ LEFT JOIN”,$lookup而populate()通过的 “最佳” 使用则否定了$unwind这里不保留空数组。您可以添加该preserveNullAndEmptyArrays选项,但这会否定上述的 “优化” 序列,并且基本上使通常在优化中组合的所有三个阶段保持不变。
populate()
preserveNullAndEmptyArrays
MongoDB 3.6以 “更具表现力”的 形式扩展,$lookup允许“子管道”表达。这不仅可以满足保留“ LEFT JOIN”的目标,而且还可以通过简化的语法来优化查询以减少返回的结果:
Item.aggregate([ { "$lookup": { "from": ItemTags.collection.name, "let": { "tags": "$tags" }, "pipeline": [ { "$match": { "tags": { "$in": [ "politics", "funny" ] }, "$expr": { "$in": [ "$_id", "$$tags" ] } }} ] }} ])
在$expr以匹配使用的已声明与“洋”价值“本地”值实际上是MongoDB中做什么“内部”现在与原来的$lookup语法。通过以这种形式表达,我们可以$match自己在“子管道”中定制初始表达。
$expr
实际上,作为真正的“聚合管道”,您几乎可以使用此“子管道”表达式中的聚合管道执行任何操作,包括“嵌套” $lookup其他相关集合的级别。
进一步的使用超出了这里所问问题的范围,但是对于甚至“嵌套的人口”而言,新的使用模式$lookup允许这几乎相同,而 “很多” 功能在其全部使用方面更为强大。
以下是在模型上使用静态方法的示例。一旦实现了该静态方法,则调用将变得简单:
Item.lookup( { path: 'tags', query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } } }, callback )
或增强一些甚至更现代:
let results = await Item.lookup({ path: 'tags', query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } } })
使它与.populate()结构非常相似,但实际上是在服务器上进行联接。为了完整起见,此处的用法根据父级和子级案例将返回的数据强制转换回Mongoose文档实例。
在大多数情况下,它是相当琐碎且易于适应或使用的。
注意 :此处使用async只是为了简化运行随附示例的过程。实际的实现没有这种依赖性。
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(); } )
或者,对于Node 8.x及更高版本,async/await没有任何其他依赖关系则更加现代:
async/await
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() } })()
从MongoDB 3.6及更高版本开始,即使没有$unwind和$group构建,也可以:
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() } })()