Con un moderno MongoDB maggiore di 3.2 puoi usare $lookup
in alternativa a .populate()
nella maggior parte dei casi. Questo ha anche il vantaggio di fare effettivamente il join "sul server" rispetto a ciò che .populate()
fa che in realtà è "query multiple" da "emulare" un join.
Quindi .populate()
è non davvero un "join" nel senso di come lo fa un database relazionale. La $lookup
operatore d'altra parte, fa effettivamente il lavoro sul server ed è più o meno analogo a un "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 Il .collection.name
qui valuta effettivamente la "stringa" che è il nome effettivo della raccolta MongoDB assegnata al modello. Poiché mongoose "pluralizza" i nomi delle raccolte per impostazione predefinita e $lookup
ha bisogno del nome effettivo della raccolta MongoDB come argomento (poiché si tratta di un'operazione del server), quindi questo è un trucco pratico da utilizzare nel codice mongoose, invece di "codificare direttamente" il nome della raccolta.
Mentre potremmo anche usare $filter
sugli array per rimuovere gli elementi indesiderati, questa è in realtà la forma più efficiente grazie all'ottimizzazione della pipeline di aggregazione per la condizione speciale di come $lookup
seguito da entrambi un $unwind
e un $match
condizione.
Ciò si traduce effettivamente in tre fasi della pipeline che vengono riunite in una:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
Questo è altamente ottimale poiché l'operazione effettiva "filtra prima la raccolta per unirsi", quindi restituisce i risultati e "svolge" l'array. Entrambi i metodi vengono impiegati in modo che i risultati non superino il limite BSON di 16 MB, che è un vincolo che il client non ha.
L'unico problema è che sembra "contro-intuitivo" in qualche modo, in particolare quando vuoi che i risultati siano in un array, ma questo è ciò che il $group
è per qui, poiché ricostruisce la forma del documento originale.
È anche un peccato che in questo momento semplicemente non possiamo scrivere effettivamente $lookup
nella stessa eventuale sintassi utilizzata dal server. IMHO, questa è una svista da correggere. Ma per ora, il semplice utilizzo della sequenza funzionerà ed è l'opzione più praticabile con le migliori prestazioni e scalabilità.
Addendum - MongoDB 3.6 e versioni successive
Sebbene il modello mostrato qui sia abbastanza ottimizzato a causa del modo in cui le altre fasi vengono inserite nel $lookup
, ha un errore in quanto "LEFT JOIN" che è normalmente inerente a entrambi $lookup
e le azioni di populate()
è negato da "ottimale" utilizzo di $unwind
qui che non conserva array vuoti. Puoi aggiungere preserveNullAndEmptyArrays
opzione, ma ciò annulla l'"ottimizzato" sequenza sopra descritta e sostanzialmente lascia intatte tutte e tre le fasi che normalmente verrebbero combinate nell'ottimizzazione.
MongoDB 3.6 si espande con un "più espressivo" forma di $lookup
consentendo un'espressione "sub-pipeline". Che non solo soddisfa l'obiettivo di mantenere il "LEFT JOIN" ma consente comunque una query ottimale per ridurre i risultati restituiti e con una sintassi molto semplificata:
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
Il $expr
utilizzato per abbinare il valore "locale" dichiarato con il valore "estraneo" è in realtà ciò che MongoDB fa "internamente" ora con l'originale $lookup
sintassi. Esprimendo in questo modulo possiamo personalizzare il $match
iniziale espressione all'interno della "sotto-pipeline" noi stessi.
In effetti, come una vera "conduttura di aggregazione" puoi fare qualsiasi cosa tu possa fare con una pipeline di aggregazione all'interno di questa espressione "sotto-pipeline", incluso "annidare" i livelli di $lookup
ad altre raccolte correlate.
Un ulteriore utilizzo è un po' oltre lo scopo di ciò che la domanda qui pone, ma in relazione anche alla "popolazione nidificata", il nuovo modello di utilizzo di $lookup
consente che questo sia più o meno lo stesso e un "lotto" più potente nel suo pieno utilizzo.
Esempio di lavoro
Di seguito viene fornito un esempio che utilizza un metodo statico sul modello. Una volta implementato quel metodo statico, la chiamata diventa semplicemente:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
O migliorare per essere un po' più moderno diventa anche:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Rendendolo molto simile a .populate()
nella struttura, ma in realtà sta eseguendo il join sul server. Per completezza, l'utilizzo qui restituisce i dati restituiti alle istanze del documento mangusta in base sia al caso padre che a quello figlio.
È abbastanza banale e facile da adattare o semplicemente da usare come nei casi più comuni.
NB L'uso di async qui è solo per brevità dell'esecuzione dell'esempio allegato. L'implementazione effettiva è esente da questa dipendenza.
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();
}
)
O un po' più moderno per Node 8.x e versioni successive con async/await
e nessuna dipendenza aggiuntiva:
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()
}
})()
E da MongoDB 3.6 e versioni successive, anche senza $unwind
e $group
edificio:
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()
}
})()