Mit einer modernen MongoDB größer als 3,2 können Sie in den meisten Fällen $lookup
eine Alternative zu .populate()
verwenden. Dies hat auch den Vorteil , tatsächlich tun , die Join „auf dem Server“ im Gegensatz zu dem, was .populate()
tut , was ist eigentlich „mehrere Anfragen“ zu „emulieren“ Join.
Es .populate()
handelt sich also nicht wirklich um einen "Join" im Sinne einer relationalen Datenbank. Der $lookup
Bediener hingegen erledigt die Arbeit tatsächlich auf dem Server und ist mehr oder weniger analog zu einem "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) {
}
)
NB Die .collection.name
hier wertet tatsächlich auf die „string“ , dass der tatsächliche Name der MongoDB Sammlung ist als dem Modell zugeordnet. Da Mungo standardmäßig Sammlungsnamen "pluralisiert" und $lookup
den tatsächlichen MongoDB-Sammlungsnamen als Argument benötigt (da es sich um eine Serveroperation handelt), ist dies ein praktischer Trick, der im Mungo-Code verwendet werden kann, anstatt den Sammlungsnamen direkt "hart zu codieren" .
Während wir auch $filter
Arrays verwenden könnten , um unerwünschte Elemente zu entfernen, ist dies aufgrund der Aggregation Pipeline Optimization für die spezielle Bedingung, $lookup
gefolgt von einer $unwind
und einer $match
Bedingung , tatsächlich die effizienteste Form .
Dies führt tatsächlich dazu, dass die drei Pipeline-Stufen zu einer zusammengefasst werden:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
Dies ist sehr optimal, da die eigentliche Operation "die Sammlung filtert, um sie zuerst zu verbinden", dann die Ergebnisse zurückgibt und das Array "abwickelt". Beide Methoden werden verwendet, damit die Ergebnisse die BSON-Grenze von 16 MB nicht überschreiten. Dies ist eine Einschränkung, die der Client nicht hat.
Das einzige Problem ist, dass es in gewisser Weise "kontraintuitiv" erscheint, insbesondere wenn Sie die Ergebnisse in einem Array haben möchten, aber dafür ist das $group
hier gedacht, da es in die ursprüngliche Dokumentform rekonstruiert wird.
Es ist auch bedauerlich, dass wir zu diesem Zeitpunkt einfach nicht $lookup
in der gleichen Syntax schreiben können, die der Server verwendet. IMHO, dies ist ein Versehen, das korrigiert werden muss. Im Moment funktioniert die einfache Verwendung der Sequenz und ist die praktikabelste Option mit der besten Leistung und Skalierbarkeit.
Nachtrag - MongoDB 3.6 und höher
Obwohl das hier gezeigte Muster aufgrund der Art und Weise, wie die anderen Stufen in die Rolle gerollt werden , ziemlich optimiert ist$lookup
, hat es einen Fehler darin, dass der "LEFT JOIN", der normalerweise beiden inhärent ist, $lookup
und die Aktionen von populate()
durch die "optimale" Verwendung von negiert werden $unwind
hier, wo leere Arrays nicht erhalten bleiben. Sie können die preserveNullAndEmptyArrays
Option hinzufügen , dies negiert jedoch die oben beschriebene "optimierte" Sequenz und lässt im Wesentlichen alle drei Stufen intakt, die normalerweise bei der Optimierung kombiniert würden.
MongoDB 3.6 wird um eine "ausdrucksstärkere" Form erweitert, bei der $lookup
ein "Sub-Pipeline" -Ausdruck zugelassen wird. Dies erfüllt nicht nur das Ziel, den "LEFT JOIN" beizubehalten, sondern ermöglicht auch eine optimale Abfrage, um die zurückgegebenen Ergebnisse mit einer stark vereinfachten Syntax zu reduzieren:
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
Das, was $expr
verwendet wird, um den deklarierten "lokalen" Wert mit dem "fremden" Wert abzugleichen, ist tatsächlich das, was MongoDB jetzt "intern" mit der ursprünglichen $lookup
Syntax macht. Indem wir in dieser Form ausdrücken, können wir den anfänglichen $match
Ausdruck innerhalb der "Sub-Pipeline" selbst anpassen.
Tatsächlich können Sie als echte "Aggregationspipeline" fast alles tun, was Sie mit einer Aggregationspipeline innerhalb dieses Ausdrucks "Subpipeline" tun können, einschließlich des "Verschachtelns" der Ebenen $lookup
in andere verwandte Sammlungen.
Die weitere Verwendung geht etwas über den Rahmen der hier gestellten Frage hinaus, aber in Bezug auf selbst "verschachtelte Grundgesamtheit" ermöglicht das neue Verwendungsmuster von, $lookup
dass dies weitgehend gleich ist und "viel" leistungsfähiger in seiner vollen Verwendung ist.
Arbeitsbeispiel
Im Folgenden finden Sie ein Beispiel für die Verwendung einer statischen Methode für das Modell. Sobald diese statische Methode implementiert ist, wird der Aufruf einfach:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Oder die Verbesserung, um ein bisschen moderner zu werden, wird sogar:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Es ist der .populate()
Struktur sehr ähnlich , führt aber stattdessen den Join auf dem Server durch. Der Vollständigkeit halber werden bei der Verwendung hier die zurückgegebenen Daten gemäß den übergeordneten und untergeordneten Fällen an mongoose Dokumentinstanzen zurückgegeben.
Es ist ziemlich trivial und einfach anzupassen oder einfach zu verwenden, wie es in den meisten Fällen der Fall ist.
Hinweis: Die Verwendung von Async dient hier nur der Kürze der Ausführung des beigefügten Beispiels. Die eigentliche Implementierung ist frei von dieser Abhängigkeit.
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(
[
(callback) => async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
(callback) =>
async.waterfall(
[
(callback) =>
ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],
callback),
(tags, callback) =>
Item.create({ "title": "Something","description": "An item",
"tags": tags },callback)
],
callback
),
(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();
}
)
Oder etwas moderner für Node 8.x und höher async/await
ohne zusätzliche Abhängigkeiten:
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);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
const tags = await ItemTag.create(
["movies", "funny"].map(tagName =>({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
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()
}
})()
Und ab MongoDB 3.6 und höher, auch ohne $unwind
und $group
Gebäude:
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);
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);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
const tags = await ItemTag.insertMany(
["movies", "funny"].map(tagName => ({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
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()
}
})()