Com um MongoDB moderno maior que 3.2, você pode usar
$lookup
como uma alternativa para .populate()
na maioria dos casos. Isso também tem a vantagem de realmente fazer o join "no servidor" ao contrário do que .populate()
faz que na verdade é "várias consultas" para "emular" uma junção. Então
.populate()
é não realmente uma "junção" no sentido de como um banco de dados relacional faz isso. A $lookup
por outro lado, realmente faz o trabalho no servidor e é mais ou menos análogo a um "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. O.collection.name
aqui, na verdade, avalia a "string" que é o nome real da coleção do MongoDB conforme atribuído ao modelo. Como o mangusto "pluraliza" os nomes das coleções por padrão e$lookup
precisa do nome real da coleção do MongoDB como um argumento (já que é uma operação do servidor), então esse é um truque útil para usar no código do mongoose, em vez de "codificar diretamente" o nome da coleção.
Embora também possamos usar
$filter
em arrays para remover os itens indesejados, esta é realmente a forma mais eficiente devido à Otimização de Pipeline de Agregação para a condição especial de como $lookup
seguido por um $unwind
e um $match
doença. Isso realmente resulta nos três estágios do pipeline sendo rolados em um:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
Isso é altamente ideal, pois a operação real "filtra a coleção para ingressar primeiro", depois retorna os resultados e "desenrola" a matriz. Ambos os métodos são empregados para que os resultados não ultrapassem o limite BSON de 16 MB, que é uma restrição que o cliente não possui.
O único problema é que parece "contra-intuitivo" em alguns aspectos, principalmente quando você deseja os resultados em uma matriz, mas é isso que o
$group
é para aqui, pois reconstrói a forma do documento original. Também é lamentável que simplesmente não possamos neste momento realmente escrever
$lookup
na mesma sintaxe eventual que o servidor usa. IMHO, este é um descuido a ser corrigido. Mas, por enquanto, simplesmente usar a sequência funcionará e é a opção mais viável com o melhor desempenho e escalabilidade. Adendo - MongoDB 3.6 e superior
Embora o padrão mostrado aqui seja bastante otimizado devido a como os outros estágios são inseridos no
$lookup
, ele tem uma falha em que o "LEFT JOIN" que normalmente é inerente a ambos $lookup
e as ações de populate()
é negado pelo "ótimo" uso de $unwind
aqui que não preserva arrays vazios. Você pode adicionar os preserveNullAndEmptyArrays
opção, mas isso nega a opção "otimizada" seqüência descrita acima e essencialmente deixa todos os três estágios intactos que normalmente seriam combinados na otimização. MongoDB 3.6 expande com um "mais expressivo" forma de
$lookup
permitindo uma expressão "sub-pipeline". O que não só cumpre o objetivo de reter o "LEFT JOIN", mas ainda permite uma consulta otimizada para reduzir os resultados retornados e com uma sintaxe bem simplificada:Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
O
$expr
usado para combinar o valor "local" declarado com o valor "estrangeiro" é na verdade o que o MongoDB faz "internamente" agora com o $lookup
original sintaxe. Ao expressar neste formulário, podemos adaptar o $match
inicial expressão dentro do "sub-pipeline" nós mesmos. Na verdade, como um verdadeiro "pipeline de agregação", você pode fazer praticamente qualquer coisa com um pipeline de agregação dentro dessa expressão "sub-pipeline", incluindo "aninhar" os níveis de
$lookup
para outras coleções relacionadas. O uso adicional está um pouco além do escopo do que a pergunta aqui faz, mas em relação à "população aninhada", o novo padrão de uso de
$lookup
permite que isso seja praticamente o mesmo, e um "lote" mais poderoso em seu uso total. Exemplo de trabalho
Veja a seguir um exemplo usando um método estático no modelo. Uma vez que esse método estático é implementado, a chamada simplesmente se torna:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Ou melhorar para ser um pouco mais moderno ainda se torna:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Tornando-o muito semelhante a
.populate()
na estrutura, mas na verdade está fazendo a junção no servidor. Para completar, o uso aqui lança os dados retornados de volta para as instâncias do documento mangusto de acordo com os casos pai e filho. É bastante trivial e fácil de adaptar ou apenas usar como é para os casos mais comuns.
N.B O uso de assíncrono aqui é apenas para brevidade de execução do exemplo incluído. A implementação real está livre dessa dependência.
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 um pouco mais moderno para o Node 8.xe acima com
async/await
e sem dependências adicionais: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 do MongoDB 3.6 e superior, mesmo sem o
$unwind
e $group
prédio: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()
}
})()