Mit einer modernen MongoDB größer als 3.2 können Sie $lookup
verwenden als Alternative zu .populate()
in den meisten Fällen. Dies hat auch den Vorteil, dass der Join tatsächlich "auf dem Server" durchgeführt wird, im Gegensatz zu .populate()
tut, was eigentlich "mehrere Abfragen" zu "emulieren" sind ein Beitritt.
Also .populate()
ist nicht wirklich ein "Join" im Sinne einer relationalen Datenbank. Die $lookup
Operator hingegen erledigt eigentlich die Arbeit 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) {
// "tags" is now filtered by condition and "joined"
}
)
NB. Der .collection.name
Hier wird tatsächlich die "Zeichenfolge" ausgewertet, die der tatsächliche Name der MongoDB-Sammlung ist, wie sie dem Modell zugewiesen ist. Da Mongoose Sammlungsnamen standardmäßig "pluralisiert" und $lookup
den tatsächlichen Namen der MongoDB-Sammlung als Argument benötigt ( da es sich um eine Serveroperation handelt ), dann ist dies ein praktischer Trick, der im Mongoose-Code verwendet werden kann, im Gegensatz zur direkten "Festcodierung" des Sammlungsnamens.
Wir könnten aber auch $filter
verwenden auf Arrays, um die unerwünschten Elemente zu entfernen, ist dies tatsächlich die effizienteste Form aufgrund der Aggregation Pipeline-Optimierung für die spezielle Bedingung von als $lookup
gefolgt von einem $unwind
und ein $match
Zustand.
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 zu verbindende Sammlung zuerst filtert", dann die Ergebnisse zurückgibt und das Array "entwindet". Beide Methoden werden verwendet, damit die Ergebnisse die BSON-Grenze von 16 MB nicht überschreiten, was eine Einschränkung ist, 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 das ist es, was die $group
ist für hier, da es die ursprüngliche Dokumentform rekonstruiert.
Es ist auch bedauerlich, dass wir zu diesem Zeitpunkt einfach nicht $lookup
schreiben können in der gleichen eventuellen Syntax, die der Server verwendet. IMHO, das ist ein Versehen, das korrigiert werden muss. Aber 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 ziemlich optimiert ist aufgrund dessen, wie die anderen Stufen in $lookup
gerollt werden , es hat einen Fehler, nämlich das "LEFT JOIN", das normalerweise beiden $lookup
innewohnt und die Aktionen von populate()
wird durch das "optimal" negiert Verwendung von $unwind
Hier werden keine leeren Arrays beibehalten. Sie können preserveNullAndEmptyArrays
hinzufügen Option, aber dies negiert das "optimiert" oben beschriebene Reihenfolge und lässt im Wesentlichen alle drei Stufen intakt, die normalerweise bei der Optimierung kombiniert würden.
MongoDB 3.6 wird um ein "ausdrucksstärkeres" erweitert Form von $lookup
Zulassen eines "Sub-Pipeline"-Ausdrucks. Das erfüllt nicht nur das Ziel, den "LEFT JOIN" beizubehalten, sondern ermöglicht dennoch eine optimale Abfrage, um die zurückgegebenen Ergebnisse zu reduzieren, und das mit einer stark vereinfachten Syntax:
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
Der $expr
verwendet wird, um den deklarierten „lokalen“ Wert mit dem „fremden“ Wert abzugleichen, ist eigentlich das, was MongoDB jetzt „intern“ mit dem ursprünglichen $lookup
macht Syntax. Indem wir diese Form ausdrücken, können wir das anfängliche $match
anpassen Ausdruck innerhalb der "Sub-Pipeline" selbst.
Tatsächlich können Sie als echte „Aggregationspipeline“ fast alles tun, was Sie mit einer Aggregationspipeline innerhalb dieses „Sub-Pipeline“-Ausdrucks tun können, einschließlich des „Verschachtelns“ der Ebenen von $lookup
zu anderen verwandten Sammlungen.
Die weitere Verwendung geht etwas über den Rahmen dessen hinaus, was die Frage hier stellt, aber in Bezug auf sogar "verschachtelte Population" dann das neue Verwendungsmuster von $lookup
ermöglicht, dass dies ziemlich gleich ist, und ein "viel" leistungsstärker bei voller Nutzung.
Arbeitsbeispiel
Im Folgenden finden Sie ein Beispiel mit einer statischen Methode für das Modell. Sobald diese statische Methode implementiert ist, wird der Aufruf einfach zu:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Oder etwas moderner zu machen wird sogar:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Dadurch ist es .populate()
sehr ähnlich in der Struktur, aber es führt stattdessen den Join auf dem Server durch. Der Vollständigkeit halber wirft die Verwendung hier die zurückgegebenen Daten zurück in die Mongoose-Dokumentinstanzen, und zwar entsprechend sowohl den übergeordneten als auch den untergeordneten Fällen.
Es ist ziemlich trivial und einfach anzupassen oder einfach so zu verwenden, wie es für die meisten gängigen Fälle ist.
NB Die Verwendung von async hier dient 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(
[
// 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();
}
)
Oder etwas moderner für Node 8.x und höher mit async/await
und keine zusätzlichen 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);
// 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()
}
})()
Und ab MongoDB 3.6 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);
// 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()
}
})()