Met een moderne MongoDB groter dan 3.2 kunt u $lookup
. gebruiken als alternatief voor .populate()
in de meeste gevallen. Dit heeft ook het voordeel dat de join daadwerkelijk "op de server" wordt uitgevoerd in tegenstelling tot wat .populate()
doet wat eigenlijk "meerdere zoekopdrachten" is om te "emuleren" een deelname.
Dus .populate()
is niet echt een "join" in de zin van hoe een relationele database het doet. De $lookup
operator daarentegen doet het werk op de server en is min of meer analoog aan een "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"
}
)
NB De .collection.name
hier evalueert feitelijk naar de "string" die de werkelijke naam is van de MongoDB-verzameling zoals toegewezen aan het model. Omdat mangoest standaard collectienamen "pluraliseert" en $lookup
de daadwerkelijke MongoDB-verzamelingsnaam als argument nodig heeft (aangezien het een serverbewerking is), dan is dit een handige truc om te gebruiken in mangoestcode, in tegenstelling tot het "hard coderen" van de verzamelingsnaam rechtstreeks.
Hoewel we ook $filter
. kunnen gebruiken op arrays om de ongewenste items te verwijderen, dit is eigenlijk de meest efficiënte vorm vanwege Aggregation Pipeline Optimization voor de speciale voorwaarde van als $lookup
gevolgd door zowel een $unwind
en een $match
staat.
Dit resulteert er feitelijk in dat de drie pijplijnfasen in één worden samengevoegd:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
Dit is zeer optimaal omdat de eigenlijke bewerking "de verzameling filtert om eerst samen te voegen", vervolgens de resultaten retourneert en de array "afwikkelt". Beide methoden worden gebruikt, zodat de resultaten de BSON-limiet van 16 MB niet overschrijden, wat een beperking is die de client niet heeft.
Het enige probleem is dat het in sommige opzichten "contra-intuïtief" lijkt, vooral als je de resultaten in een array wilt, maar dat is wat de $group
is voor hier, omdat het wordt gereconstrueerd naar de originele documentvorm.
Het is ook jammer dat we op dit moment gewoon niet echt $lookup
kunnen schrijven in dezelfde uiteindelijke syntaxis die de server gebruikt. IMHO, dit is een vergissing die moet worden gecorrigeerd. Maar voor nu werkt het eenvoudigweg gebruik van de reeks en is dit de meest haalbare optie met de beste prestaties en schaalbaarheid.
Aanvulling - MongoDB 3.6 en hoger
Hoewel het hier getoonde patroon redelijk geoptimaliseerd . is vanwege de manier waarop de andere fasen in de $lookup
. worden gerold , het heeft één tekortkoming in die zin dat de "LEFT JOIN" die normaal inherent is aan zowel $lookup
en de acties van populate()
wordt teniet gedaan door de "optimale" gebruik van $unwind
hier die geen lege arrays behoudt. U kunt de preserveNullAndEmptyArrays
. toevoegen optie, maar dit negeert de "geoptimaliseerde" hierboven beschreven en laat in wezen alle drie de fasen intact die normaal gesproken zouden worden gecombineerd in de optimalisatie.
MongoDB 3.6 breidt uit met een "meer expressieve" vorm van $lookup
het toestaan van een "sub-pijplijn"-expressie. Wat niet alleen voldoet aan het doel om de "LEFT JOIN" te behouden, maar nog steeds een optimale zoekopdracht mogelijk maakt om de geretourneerde resultaten te verminderen en met een sterk vereenvoudigde syntaxis:
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
De $expr
gebruikt om de gedeclareerde "lokale" waarde te matchen met de "buitenlandse" waarde is eigenlijk wat MongoDB nu "intern" doet met de originele $lookup
syntaxis. Door in deze vorm uit te drukken, kunnen we de initiële $match
. aanpassen uitdrukking binnen de "sub-pijplijn" zelf.
Als echte "aggregatiepijplijn" kun je in feite alles doen wat je kunt doen met een aggregatiepijplijn binnen deze "subpijplijn"-expressie, inclusief het "nesten" van de niveaus van $lookup
naar andere gerelateerde collecties.
Verder gebruik valt een beetje buiten het bestek van wat de vraag hier stelt, maar zelfs met betrekking tot "geneste populatie" dan is het nieuwe gebruikspatroon van $lookup
staat toe dat dit vrijwel hetzelfde is, en een "lot" krachtiger in zijn volledige gebruik.
Werkvoorbeeld
Het volgende geeft een voorbeeld met behulp van een statische methode op het model. Zodra die statische methode is geïmplementeerd, wordt de aanroep eenvoudig:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Of verbeteren om een beetje moderner te zijn wordt zelfs:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Waardoor het erg lijkt op .populate()
in structuur, maar het doet in plaats daarvan de join op de server. Voor de volledigheid:het gebruik hier werpt de geretourneerde gegevens terug naar de documentinstanties van mangoesten volgens zowel de bovenliggende als de onderliggende gevallen.
Het is vrij triviaal en gemakkelijk aan te passen of gewoon te gebruiken zoals in de meeste gevallen.
NB Het gebruik van asynchrone hier is alleen voor de beknoptheid van het uitvoeren van het bijgevoegde voorbeeld. De daadwerkelijke implementatie is vrij van deze afhankelijkheid.
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();
}
)
Of een beetje moderner voor Node 8.x en hoger met async/await
en geen extra afhankelijkheden:
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()
}
})()
En vanaf MongoDB 3.6 en hoger, zelfs zonder de $unwind
en $group
gebouw:
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()
}
})()