Avec un MongoDB moderne supérieur à 3.2, vous pouvez utiliser $lookup
comme alternative à .populate()
dans la plupart des cas. Cette méthode présente également l'avantage d'effectuer la jointure "sur le serveur", contrairement à ce qui se passe dans le cas de la jointure de données. .populate()
fait qui est en fait "requêtes multiples" pour "émuler". un joint.
Alors .populate()
es no Il ne s'agit pas vraiment d'une "jointure" au sens où l'entend une base de données relationnelle. Le site $lookup
d'autre part, effectue réellement le travail sur le serveur, et est plus ou moins analogue à un opérateur de "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"
}
)
N.B. Le site .collection.name
évalue en fait la "chaîne" qui est le nom réel de la collection MongoDB tel qu'il est attribué au modèle. Puisque Mongoose "pluralise" les noms de collection par défaut et que $lookup
a besoin du nom de la collection MongoDB en tant qu'argument (puisqu'il s'agit d'une opération serveur), c'est une astuce pratique à utiliser dans le code Mongoose, plutôt que de coder directement le nom de la collection.
Alors que nous pourrions également utiliser $filter
sur les tableaux pour supprimer les éléments non désirés, c'est en fait la forme la plus efficace en raison des éléments suivants Optimisation du pipeline d'agrégation pour la condition spéciale de l'as $lookup
suivi à la fois d'un $unwind
et un $match
condition.
En fait, les trois étapes du pipeline sont regroupées en une seule :
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
C'est très optimal car l'opération actuelle "filtre la collection à joindre en premier", puis renvoie les résultats et "déroule" le tableau. Les deux méthodes sont employées pour que les résultats ne dépassent pas la limite BSON de 16 Mo, une contrainte que le client n'a pas.
Le seul problème est que cela semble "contre-intuitif" à certains égards, en particulier lorsque l'on veut obtenir les résultats dans un tableau, mais c'est ce que fait la fonction $group
est pour ici, car il reconstruit à la forme du document original.
Il est également regrettable que nous ne puissions tout simplement pas, à l'heure actuelle, écrire $lookup
dans la même syntaxe éventuelle que celle utilisée par le serveur. IMHO, il s'agit d'un oubli à corriger. Mais pour l'instant, utiliser simplement la séquence fonctionnera et constitue l'option la plus viable avec les meilleures performances et la meilleure évolutivité.
Addendum - MongoDB 3.6 et versions ultérieures
Bien que le modèle présenté ici soit assez optimisé en raison de la manière dont les autres étapes sont intégrées dans la $lookup
mais il présente un défaut : le "LEFT JOIN", qui est normalement inhérent aux deux types d'utilisateurs, ne peut être utilisé. $lookup
et les actions de populate()
est annulé par le "optimal" l'usage de $unwind
ici qui ne préserve pas les tableaux vides. Vous pouvez ajouter le preserveNullAndEmptyArrays
mais cela annule l'option "optimisé" décrit ci-dessus et laisse essentiellement intactes les trois étapes qui seraient normalement combinées dans l'optimisation.
MongoDB 3.6 s'étend avec une "plus expressif" forme de $lookup
permettant une expression "sous-pipeline". Ce qui non seulement répond à l'objectif de conserver le "LEFT JOIN" mais permet aussi une requête optimale pour réduire les résultats retournés et avec une syntaxe beaucoup plus simple :
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
En $expr
utilisé afin de faire correspondre la valeur "locale" déclarée avec la valeur "étrangère" est en fait ce que MongoDB fait "en interne" maintenant avec la valeur originale $lookup
syntaxe. En l'exprimant sous cette forme, nous pouvons adapter l'initial $match
à l'intérieur même de la "sous-pipeline".
En fait, en tant que véritable "pipeline d'agrégation", vous pouvez faire à peu près tout ce que vous pouvez faire avec un pipeline d'agrégation dans cette expression de "sous-pipeline", y compris "imbriquer" les niveaux de $lookup
à d'autres collections connexes.
Une utilisation plus poussée dépasse un peu le cadre de la question posée ici, mais en ce qui concerne même la "population imbriquée", le nouveau schéma d'utilisation de $lookup
permet de le faire à peu près de la même façon, et une "lot" plus puissant dans sa pleine utilisation.
Exemple de travail
Voici un exemple d'utilisation d'une méthode statique sur le modèle. Une fois que cette méthode statique est implémentée, l'appel devient simplement :
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Ou l'amélioration pour être un peu plus moderne devient même :
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Ce qui le rend très similaire à .populate()
dans la structure, mais il fait en fait la jointure sur le serveur à la place. Pour être complet, l'utilisation ici reconvertit les données retournées en instances de documents de la mangouste selon les cas parent et enfant.
Il est assez trivial et facile à adapter ou à utiliser tel quel pour les cas les plus courants.
N.B. L'utilisation de asynchrone ici, c'est juste pour la brièveté de l'exécution de l'exemple ci-joint. L'implémentation réelle est libre de cette dépendance.
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();
}
)
Ou un peu plus moderne pour Node 8.x et plus avec async/await
et aucune dépendance supplémentaire :
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()
}
})()
Et à partir de MongoDB 3.6, même sans l'option $unwind
y $group
bâtiment :
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()
}
})()