我有一个mongoose Group模式,其中包含被邀请者(子文档数组)和currentMove,被邀请者还包含currentMove,我想获得只有具有相同currentMove的子文档的文档.
Group.findById("5a03fa29fafa645c8a399353")
.populate({
path: 'invitee.user_id',
select: 'currentMove',
model:"User",
match: {
"currentMove":{
$eq: "$currentMove"
}
}
})
这会为匹配查询生成未知的currentMove Object id.我不确定mongoose是否具有此功能.有人可以帮帮我吗?
最佳答案 在现代MongoDB版本中,使用
$lookup
而不是.populate()更有效.您想要根据字段比较进行过滤的基本概念是MongoDB与本机运算符完美匹配的东西,但它不是您可以轻松转换为.populate()的东西.
实际上,与.populate()实际使用的唯一方法是首先检索所有结果,然后使用Model.populate()和查询的$where
子句,同时按顺序处理结果数组Array.map()将每个文档的本地值应用于“加入”的条件.
它有点混乱,涉及从服务器中提取所有结果并在本地过滤.所以$lookup
是我们最好的选择,其中所有“过滤”和“匹配”实际上都发生在服务器上,而不需要通过网络提取不必要的文档只是为了获得结果.
示例模式
您实际上并未在问题中包含“架构”,因此我们只能根据您实际在问题中包含的部分进行近似处理.所以这里我的例子使用:
const userSchema = new Schema({
name: String,
currentMove: Number
})
const groupSchema = new Schema({
name: String,
topic: String,
currentMove: Number,
invitee: [{
user_id: { type: Schema.Types.ObjectId, ref: 'User' },
confirmed: { type: Boolean, default: false }
}]
});
展开$lookup和$group
从这里开始,我们对$lookup
查询有不同的方法.第一个基本上涉及在$lookup
阶段之前和之后应用$unwind
.这部分是因为你的“引用”是数组中的一个嵌入字段,部分原因是因为它实际上是最有效的查询形式,这里使用的可能“连接”结果可能超过BSON限制(文档为16MB)被避免:
Group.aggregate([
{ "$unwind": "$invitee" },
{ "$lookup": {
"from": User.collection.name,
"localField": "invitee.user_id",
"foreignField": "_id",
"as": "invitee.user_id"
}},
{ "$unwind": "$invitee.user_id" },
{ "$redact": {
"$cond": {
"if": { "$eq": ["$currentMove", "$invitee.user_id.currentMove"] },
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$group": {
"_id": "$_id",
"name": { "$first": "$name" },
"topic": { "$first": "$topic" },
"currentMove": { "$first": "$currentMove" },
"invitee": { "$push": "$invitee" }
}}
]);
这里的关键表达式是$redact
,它在返回$lookup
结果后处理.这允许逻辑比较父文档中的“currentMove”值和User对象的“已连接”细节.
从$unwind
我们的数组内容开始,我们使用$group
和$push
来重建数组(如果必须的话)并使用$first
选择原始文档的其他字段.
有一些方法可以检查模式并生成这样一个阶段,但这并不是问题的范围.可以在Querying after populate in Mongoose上看到一个示例.请注意,如果您想要返回的字段,那么您将构建此管道阶段,使用这些表达式返回原始形状的文档.
过滤$查找结果
另一种方法是,您确定“加入”的“未过滤”结果不会导致文档超出BSON限制,而是创建一个单独的目标数组,然后使用$map
和$filter
重新构建“已加入”数组内容和其他数组运算符:
Group.aggregate([
{ "$lookup": {
"from": User.collection.name,
"localField": "invitee.user_id",
"foreignField": "_id",
"as": "inviteeT"
}},
{ "$addFields": {
"invitee": {
"$map": {
"input": {
"$filter": {
"input": "$inviteeT",
"as": "i",
"cond": { "$eq": ["$$i.currentMove","$currentMove"] }
}
},
"as": "i",
"in": {
"_id": {
"$arrayElemAt": [
"$invitee._id",
{ "$indexOfArray": ["$invitee.user_id", "$$i._id"] }
]
},
"user_id": "$$i",
"confirmed": {
"$arrayElemAt": [
"$invitee.confirmed",
{ "$indexOfArray": ["$invitee.user_id","$$i._id"] }
]
}
}
}
}
}},
{ "$project": { "inviteeT": 0 } },
{ "$match": { "invitee.0": { "$exists": true } } }
]);
而不是将过滤“文档”的$redact
,我们在这里使用$filter
,表达式仅返回共享相同“currentMove”的目标数组“inviteeT”的那些成员.由于这只是“外来”内容,我们使用$map
与原始数组“连接”并转置元素.
为了从原始数组中“转置”值,我们使用$arrayElemAt
和$indexOfArray
表达式. $indexOfArray
允许我们将目标的“_id”值与原始数组中的“user_id”值进行匹配,并获得它的“索引”位置.我们总是知道这会返回真正的匹配,因为$lookup
为我们做了那部分.
然后将“索引”值提供给$arrayElemAt
,类似地将值的“映射”应用为类似“$invitee.confirmed”的数组,并返回在相同索引处匹配的值.这基本上是数组之间的“查找”.
与第一个管道示例不同,我们现在仍然拥有“inviteeT”数组以及我们重新编写的“被邀请者”数组,由$addFields
提供.因此,摆脱这种情况的一种方法是添加额外的$project
并排除不需要的“临时“阵列.当然,因为我们没有$unwind
和“过滤器”,所以仍然可能没有匹配的数组条目.因此,$match
表达式使用$exists
来测试数组结果中存在的0索引,这意味着存在“至少一个”结果,并丢弃具有空数组的任何文档.
MongoDB 3.6“子查询”
MongoDB 3.6使得它更清晰,因为$lookup
的新语法允许在参数中给出更具表现力的“管道”以选择返回的结果,而不是简单的“localField”和“foreignField”匹配.
Group.aggregate([
{ "$lookup": {
"from": User.collection.name,
"let": {
"ids": "$invitee._id",
"users": "$invitee.user_id",
"confirmed": "$invitee.confirmed",
"currentMove": "$currentMove"
},
"pipeline": [
{ "$match": {
"$expr": {
"$and": [
{ "$in": ["$_id", "$$users"] },
{ "$eq": ["$currentMove", "$$currentMove"] }
]
}
}},
{ "$project": {
"_id": {
"$arrayElemAt": [
"$$ids",
{ "$indexOfArray": ["$$users", "$_id"] }
]
},
"user_id": "$$ROOT",
"confirmed": {
"$arrayElemAt": [
"$$confirmed",
{ "$indexOfArray": ["$$users", "$_id"] }
]
}
}}
],
"as": "invitee"
}},
{ "$match": { "invitee.0": { "$exists": true } } }
])
因此,由于当前如何通过“let”声明将这些数据传递到子管道,因此使用特定输入值的映射数组时,会出现一些稍微“微不足道”的事情.这可能应该更干净,但在当前的候选版本中,这是实际需要表达以便工作的方式.
使用这种新语法,“let”允许我们从当前文档声明“变量”,然后可以在“管道”表达式中引用该表达式,该表达式将被执行以确定返回目标数组的结果.
$expr
在这里基本上取代了之前使用的$redact
或$filter
条件,以及将“本地”与“外部”键匹配相结合,这也需要我们声明这样的变量.在这里,我们将源文档中的“$invitee.user_id”值映射到一个变量中,我们在其余表达式中将其称为“$$users”.
这里的$in
运算符是聚合框架的变体,它返回一个布尔条件,其中第一个参数“value”在第二个参数“array”中找到.所以这是“外键”过滤器部分.
由于这是一个“管道”,除了$match
之外,我们还可以添加一个$project
阶段,该阶段从国外集合中选择了项目.因此,我们再次使用与之前描述的类似的“换位”技术.然后,这使我们能够控制数组中返回的文档的“形状”,因此我们不会像之前那样在$lookup之后“操纵”返回的数组.
同样的情况适用,因为无论你在这里做什么,“子管道”当然可以在过滤条件不匹配时不返回任何结果.因此,使用相同的$exists
测试来丢弃这些文档.
所以这一切都非常酷,一旦你习惯了服务器端可用的电源“加入”$lookup的功能,你很可能永远不会回头.虽然语法比引入.populate()的“便利”功能简洁得多,但流量负载减少,更高级的用途和一般表现力基本上弥补了这一点.
作为一个完整的例子,我还包括一个自包含的列表,演示了所有这些.如果您使用附加的MongoDB 3.6兼容服务器运行它,那么您甚至可以进行该演示.
需要最近的Node.js v8.x版本与async / await一起运行(或者在其他支持的情况下启用),但是因为现在是LTS版本,你真的应该正在运行它.至少安装一个测试:)
const mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const uri = 'mongodb://localhost/rollgroup',
options = { useMongoClient: true };
const userSchema = new Schema({
name: String,
currentMove: Number
})
const groupSchema = new Schema({
name: String,
topic: String,
currentMove: Number,
invitee: [{
user_id: { type: Schema.Types.ObjectId, ref: 'User' },
confirmed: { type: Boolean, default: false }
}]
});
const User = mongoose.model('User', userSchema);
const Group = mongoose.model('Group', groupSchema);
function log(data) {
console.log(JSON.stringify(data, undefined, 2))
}
(async function() {
try {
const conn = await mongoose.connect(uri,options);
let { version } = await conn.db.admin().command({'buildInfo': 1});
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.remove() )
);
// Add some users
let users = await User.insertMany([
{ name: 'Bill', currentMove: 1 },
{ name: 'Ted', currentMove: 2 },
{ name: 'Fred', currentMove: 3 },
{ name: 'Sally', currentMove: 4 },
{ name: 'Harry', currentMove: 5 }
]);
await Group.create({
name: 'Group1',
topic: 'This stuff',
currentMove: 3,
invitee: users.map( u =>
({ user_id: u._id, confirmed: (u.currentMove === 3) })
)
});
await (async function() {
console.log('Unwinding example');
let result = await Group.aggregate([
{ "$unwind": "$invitee" },
{ "$lookup": {
"from": User.collection.name,
"localField": "invitee.user_id",
"foreignField": "_id",
"as": "invitee.user_id"
}},
{ "$unwind": "$invitee.user_id" },
{ "$redact": {
"$cond": {
"if": { "$eq": ["$currentMove", "$invitee.user_id.currentMove"] },
"then": "$$KEEP",
"else": "$$PRUNE"
}
}},
{ "$group": {
"_id": "$_id",
"name": { "$first": "$name" },
"topic": { "$first": "$topic" },
"currentMove": { "$first": "$currentMove" },
"invitee": { "$push": "$invitee" }
}}
]);
log(result);
})();
await (async function() {
console.log('Using $filter example');
let result = await Group.aggregate([
{ "$lookup": {
"from": User.collection.name,
"localField": "invitee.user_id",
"foreignField": "_id",
"as": "inviteeT"
}},
{ "$addFields": {
"invitee": {
"$map": {
"input": {
"$filter": {
"input": "$inviteeT",
"as": "i",
"cond": { "$eq": ["$$i.currentMove","$currentMove"] }
}
},
"as": "i",
"in": {
"_id": {
"$arrayElemAt": [
"$invitee._id",
{ "$indexOfArray": ["$invitee.user_id", "$$i._id"] }
]
},
"user_id": "$$i",
"confirmed": {
"$arrayElemAt": [
"$invitee.confirmed",
{ "$indexOfArray": ["$invitee.user_id","$$i._id"] }
]
}
}
}
}
}},
{ "$project": { "inviteeT": 0 } },
{ "$match": { "invitee.0": { "$exists": true } } }
]);
log(result);
})();
await (async function() {
if (parseFloat(version.match(/\d\.\d/)[0]) >= 3.6) {
console.log('New $lookup example. Yay!');
let result = await Group.collection.aggregate([
{ "$lookup": {
"from": User.collection.name,
"let": {
"ids": "$invitee._id",
"users": "$invitee.user_id",
"confirmed": "$invitee.confirmed",
"currentMove": "$currentMove"
},
"pipeline": [
{ "$match": {
"$expr": {
"$and": [
{ "$in": ["$_id", "$$users"] },
{ "$eq": ["$currentMove", "$$currentMove"] }
]
}
}},
{ "$project": {
"_id": {
"$arrayElemAt": [
"$$ids",
{ "$indexOfArray": ["$$users", "$_id"] }
]
},
"user_id": "$$ROOT",
"confirmed": {
"$arrayElemAt": [
"$$confirmed",
{ "$indexOfArray": ["$$users", "$_id"] }
]
}
}}
],
"as": "invitee"
}},
{ "$match": { "invitee.0": { "$exists": true } } }
]).toArray();
log(result);
}
})();
await (async function() {
console.log("Horrible populate example :(");
let results = await Group.find();
results = await Promise.all(
results.map( r =>
User.populate(r,{
path: 'invitee.user_id',
match: { "$where": `this.currentMove === ${r.currentMove}` }
})
)
);
console.log("All members still there");
log(results);
// Then we clean it for null values
results = results.map( r =>
Object.assign(r,{
invitee: r.invitee.filter(i => i.user_id !== null)
})
);
console.log("Now they are filtered");
log(results);
})();
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})()
为每个示例提供输出:
Mongoose: users.remove({}, {})
Mongoose: groups.remove({}, {})
Mongoose: users.insertMany([ { __v: 0, name: 'Bill', currentMove: 1, _id: 5a0afda01643cf41789e500a }, { __v: 0, name: 'Ted', currentMove: 2, _id: 5a0afda01643cf41789e500b }, { __v: 0, name: 'Fred', currentMove: 3, _id: 5a0afda01643cf41789e500c }, { __v: 0, name: 'Sally', currentMove: 4, _id: 5a0afda01643cf41789e500d }, { __v: 0, name: 'Harry', currentMove: 5, _id: 5a0afda01643cf41789e500e } ], {})
Mongoose: groups.insert({ name: 'Group1', topic: 'This stuff', currentMove: 3, _id: ObjectId("5a0afda01643cf41789e500f"), invitee: [ { user_id: ObjectId("5a0afda01643cf41789e500a"), _id: ObjectId("5a0afda01643cf41789e5014"), confirmed: false }, { user_id: ObjectId("5a0afda01643cf41789e500b"), _id: ObjectId("5a0afda01643cf41789e5013"), confirmed: false }, { user_id: ObjectId("5a0afda01643cf41789e500c"), _id: ObjectId("5a0afda01643cf41789e5012"), confirmed: true }, { user_id: ObjectId("5a0afda01643cf41789e500d"), _id: ObjectId("5a0afda01643cf41789e5011"), confirmed: false }, { user_id: ObjectId("5a0afda01643cf41789e500e"), _id: ObjectId("5a0afda01643cf41789e5010"), confirmed: false } ], __v: 0 })
Unwinding example
Mongoose: groups.aggregate([ { '$unwind': '$invitee' }, { '$lookup': { from: 'users', localField: 'invitee.user_id', foreignField: '_id', as: 'invitee.user_id' } }, { '$unwind': '$invitee.user_id' }, { '$redact': { '$cond': { if: { '$eq': [ '$currentMove', '$invitee.user_id.currentMove' ] }, then: '$$KEEP', else: '$$PRUNE' } } }, { '$group': { _id: '$_id', name: { '$first': '$name' }, topic: { '$first': '$topic' }, currentMove: { '$first': '$currentMove' }, invitee: { '$push': '$invitee' } } } ], {})
[
{
"_id": "5a0afda01643cf41789e500f",
"name": "Group1",
"topic": "This stuff",
"currentMove": 3,
"invitee": [
{
"user_id": {
"_id": "5a0afda01643cf41789e500c",
"__v": 0,
"name": "Fred",
"currentMove": 3
},
"_id": "5a0afda01643cf41789e5012",
"confirmed": true
}
]
}
]
Using $filter example
Mongoose: groups.aggregate([ { '$lookup': { from: 'users', localField: 'invitee.user_id', foreignField: '_id', as: 'inviteeT' } }, { '$addFields': { invitee: { '$map': { input: { '$filter': { input: '$inviteeT', as: 'i', cond: { '$eq': [ '$$i.currentMove', '$currentMove' ] } } }, as: 'i', in: { _id: { '$arrayElemAt': [ '$invitee._id', { '$indexOfArray': [ '$invitee.user_id', '$$i._id' ] } ] }, user_id: '$$i', confirmed: { '$arrayElemAt': [ '$invitee.confirmed', { '$indexOfArray': [ '$invitee.user_id', '$$i._id' ] } ] } } } } } }, { '$project': { inviteeT: 0 } }, { '$match': { 'invitee.0': { '$exists': true } } } ], {})
[
{
"_id": "5a0afda01643cf41789e500f",
"name": "Group1",
"topic": "This stuff",
"currentMove": 3,
"invitee": [
{
"_id": "5a0afda01643cf41789e5012",
"user_id": {
"_id": "5a0afda01643cf41789e500c",
"__v": 0,
"name": "Fred",
"currentMove": 3
},
"confirmed": true
}
],
"__v": 0
}
]
New $lookup example. Yay!
Mongoose: groups.aggregate([ { '$lookup': { from: 'users', let: { ids: '$invitee._id', users: '$invitee.user_id', confirmed: '$invitee.confirmed', currentMove: '$currentMove' }, pipeline: [ { '$match': { '$expr': { '$and': [ { '$in': [ '$_id', '$$users' ] }, { '$eq': [ '$currentMove', '$$currentMove' ] } ] } } }, { '$project': { _id: { '$arrayElemAt': [ '$$ids', { '$indexOfArray': [ '$$users', '$_id' ] } ] }, user_id: '$$ROOT', confirmed: { '$arrayElemAt': [ '$$confirmed', { '$indexOfArray': [ '$$users', '$_id' ] } ] } } } ], as: 'invitee' } }, { '$match': { 'invitee.0': { '$exists': true } } } ])
[
{
"_id": "5a0afda01643cf41789e500f",
"name": "Group1",
"topic": "This stuff",
"currentMove": 3,
"invitee": [
{
"_id": "5a0afda01643cf41789e5012",
"user_id": {
"_id": "5a0afda01643cf41789e500c",
"__v": 0,
"name": "Fred",
"currentMove": 3
},
"confirmed": true
}
],
"__v": 0
}
]
Horrible populate example :(
Mongoose: groups.find({}, { fields: {} })
Mongoose: users.find({ _id: { '$in': [ ObjectId("5a0afda01643cf41789e500a"), ObjectId("5a0afda01643cf41789e500b"), ObjectId("5a0afda01643cf41789e500c"), ObjectId("5a0afda01643cf41789e500d"), ObjectId("5a0afda01643cf41789e500e") ] }, '$where': 'this.currentMove === 3' }, { fields: {} })
All members still there
[
{
"_id": "5a0afda01643cf41789e500f",
"name": "Group1",
"topic": "This stuff",
"currentMove": 3,
"__v": 0,
"invitee": [
{
"user_id": null,
"_id": "5a0afda01643cf41789e5014",
"confirmed": false
},
{
"user_id": null,
"_id": "5a0afda01643cf41789e5013",
"confirmed": false
},
{
"user_id": {
"_id": "5a0afda01643cf41789e500c",
"__v": 0,
"name": "Fred",
"currentMove": 3
},
"_id": "5a0afda01643cf41789e5012",
"confirmed": true
},
{
"user_id": null,
"_id": "5a0afda01643cf41789e5011",
"confirmed": false
},
{
"user_id": null,
"_id": "5a0afda01643cf41789e5010",
"confirmed": false
}
]
}
]
Now they are filtered
[
{
"_id": "5a0afda01643cf41789e500f",
"name": "Group1",
"topic": "This stuff",
"currentMove": 3,
"__v": 0,
"invitee": [
{
"user_id": {
"_id": "5a0afda01643cf41789e500c",
"__v": 0,
"name": "Fred",
"currentMove": 3
},
"_id": "5a0afda01643cf41789e5012",
"confirmed": true
}
]
}
]
使用populate()
所以在这里使用.populate()实际上非常可怕.当然它看起来更少,但它实际上做了许多根本不需要的事情,而且都是因为“加入”不会发生在服务器上:
// Note that we cannot populate "here" since we need the returned value
let results = await Group.find();
// The value is only in context as we use `Array.map()` to process each result
results = await Promise.all(
results.map( r =>
User.populate(r,{
path: 'invitee.user_id',
match: { "$where": `this.currentMove === ${r.currentMove}` }
})
)
);
console.log("All members still there");
log(results);
// Then we clean it for null values
results = results.map( r =>
Object.assign(r,{
invitee: r.invitee.filter(i => i.user_id !== null)
})
);
console.log("Now they are filtered");
log(results);
所以我还在上面的输出中包括了,以及整个代码清单.
问题变得明显,因为您无法将填充“直接”链接到第一个查询.实际上,您需要返回文档(可能是所有文档),以便在后续填充中使用当前文档值.并且必须为返回的每个文档处理.
不仅如此,但populate()不会将数组“过滤”为仅匹配的数组,即使查询条件也是如此.它所做的就是将不匹配的元素设置为null:
[
{
"_id": "5a0afa889f9f7e4064d8794d",
"name": "Group1",
"topic": "This stuff",
"currentMove": 3,
"__v": 0,
"invitee": [
{
"user_id": null,
"_id": "5a0afa889f9f7e4064d87952",
"confirmed": false
},
{
"user_id": null,
"_id": "5a0afa889f9f7e4064d87951",
"confirmed": false
},
{
"user_id": {
"_id": "5a0afa889f9f7e4064d8794a",
"__v": 0,
"name": "Fred",
"currentMove": 3
},
"_id": "5a0afa889f9f7e4064d87950",
"confirmed": true
},
{
"user_id": null,
"_id": "5a0afa889f9f7e4064d8794f",
"confirmed": false
},
{
"user_id": null,
"_id": "5a0afa889f9f7e4064d8794e",
"confirmed": false
}
]
}
]
然后,需要再次处理Array.filter()以返回“每个”文档,最终可以删除不需要的数组项,并为其他聚合查询提供相同的结果.
所以它“真的很浪费”而且不是一个很好的做事方式.当你实际在服务器上进行大部分处理时,拥有数据库的意义不大.实际上,我们可能只是简单地返回填充的结果,然后运行Array.filter()以删除不需要的条目.
这不是你如何编写快速有效的代码.所以这里的例子有时候“看起来很简单”实际上造成了更多的伤害而不是好处.