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
. $unwind
, $lookup
. , "" , , "", BSON (16 ) :
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
.
, . 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
, "inviteeT"
, "currentMove"
. "" , "" , $map
.
"" , $arrayElemAt
$indexOfArray
. $indexOfArray
"_id"
"user_id"
"index" . , , $lookup
.
"index" $arrayElemAt
"" "$invitee.confirmed"
, . "" .
, "inviteeT"
, "invitee"
$addFields
. , - $project
"" . , , $unwind
"filter", - . , $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"
"" , "pipeline"
, , , .
$expr
$redact
$filter
, , "" "" , . "$invitee.user_id"
, "$$users"
.
$in
, , "" "". , " ".
"", $project
$match
, . , "" , . "" , , "" $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});
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.remove() )
);
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);
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()
. , , , , , "join":
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);
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()
, .
, . , ", " , .