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()
}
})()
Was Sie verlangen, wird nicht direkt unterstützt, kann jedoch durch Hinzufügen eines weiteren Filterschritts nach der Rückkehr der Abfrage erreicht werden.
Erstens müssen
.populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )
Sie auf jeden Fall die Tags-Dokumente filtern. Nachdem die Abfrage zurückgegeben wurde, müssen Sie Dokumente manuell herausfiltern, die keinetags
Dokumente enthalten, die den Auffüllkriterien entsprechen. etwas wie:query.... .exec(function(err, docs){ docs = docs.filter(function(doc){ return doc.tags.length; }) // do stuff with docs });
quelle
Versuchen Sie es zu ersetzen
.populate('tags').where('tags.tagName').in(['funny', 'politics'])
durch
.populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )
quelle
Update: Bitte werfen Sie einen Blick auf die Kommentare - diese Antwort stimmt nicht richtig mit der Frage überein, beantwortet aber möglicherweise andere Fragen von Benutzern, die auf sie gestoßen sind (ich denke, das liegt an den positiven Stimmen), sodass ich diese "Antwort" nicht löschen werde:
Erstens: Ich weiß, dass diese Frage wirklich veraltet ist, aber ich habe genau nach diesem Problem gesucht und dieser SO-Beitrag war der Google-Eintrag Nr. 1. Also habe ich die
docs.filter
Version implementiert (akzeptierte Antwort), aber während ich in den Dokumenten zu Mungo v4.6.0 lese , können wir jetzt einfach Folgendes verwenden:Item.find({}).populate({ path: 'tags', match: { tagName: { $in: ['funny', 'politics'] }} }).exec((err, items) => { console.log(items.tags) // contains only tags where tagName is 'funny' or 'politics' })
Hoffe, dies hilft zukünftigen Suchmaschinenbenutzern.
quelle
fans
) wird gefiltert. Das tatsächlich zurückgegebene Dokument (das als EigenschaftStory
enthältfans
) wird nicht beeinflusst oder gefiltert.Nachdem ich kürzlich selbst das gleiche Problem hatte, habe ich die folgende Lösung gefunden:
Suchen Sie zunächst alle ItemTags, bei denen tagName entweder "lustig" oder "politisch" ist, und geben Sie ein Array von ItemTag _ids zurück.
Suchen Sie dann im Tags-Array nach Elementen, die alle ItemTag _ids enthalten
ItemTag .find({ tagName : { $in : ['funny','politics'] } }) .lean() .distinct('_id') .exec((err, itemTagIds) => { if (err) { console.error(err); } Item.find({ tag: { $all: itemTagIds} }, (err, items) => { console.log(items); // Items filtered by tagName }); });
quelle
Die Antwort von @aaronheckmann hat bei mir funktioniert, aber ich musste sie ersetzen
return doc.tags.length;
,return doc.tags != null;
da dieses Feld null enthält, wenn es nicht mit den in populate geschriebenen Bedingungen übereinstimmt. Also der endgültige Code:query.... .exec(function(err, docs){ docs = docs.filter(function(doc){ return doc.tags != null; }) // do stuff with docs });
quelle