Med en moderne MongoDB større end 3.2 kan du bruge $lookup
som en alternativ til .populate()
i de fleste tilfælde. Dette har også den fordel, at det faktisk udfører joinforbindelsen "på serveren" i modsætning til hvad .populate()
gør, hvilket faktisk er "flere forespørgsler" for at "emulere" a join.
Så .populate()
er ikke virkelig et "join" i betydningen af, hvordan en relationsdatabase gør det. $lookup
På den anden side udfører operatøren faktisk arbejdet på serveren og er mere eller mindre analog med en "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 .collection.name
her evalueres faktisk til "strengen", der er det faktiske navn på MongoDB-samlingen som tildelt modellen. Siden mongoose "pluraliserer" samlingsnavne som standard og $lookup
har brug for det faktiske MongoDB-samlingsnavn som et argument (da det er en serveroperation), så er dette et praktisk trick at bruge i mongoose-kode, i modsætning til at "hardkode" samlingsnavnet direkte.
Mens vi også kunne bruge $filter
på arrays for at fjerne de uønskede elementer, er dette faktisk den mest effektive form på grund af Aggregation Pipeline Optimization for den særlige tilstand som $lookup
efterfulgt af både en $unwind
og en $match
tilstand.
Dette resulterer faktisk i, at de tre pipeline-faser bliver rullet til én:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
Dette er yderst optimalt, da den faktiske operation "filtrerer samlingen for at slutte sig til først", så returnerer den resultaterne og "afvikler" arrayet. Begge metoder anvendes, så resultaterne ikke bryder BSON-grænsen på 16 MB, hvilket er en begrænsning, som klienten ikke har.
Det eneste problem er, at det virker "modintuitivt" på nogle måder, især når du vil have resultaterne i et array, men det er hvad $group
er til her, da den rekonstruerer til den originale dokumentform.
Det er også uheldigt, at vi på nuværende tidspunkt simpelthen ikke kan skrive $lookup
i den samme eventuelle syntaks som serveren bruger. IMHO, dette er en forglemmelse, der skal rettes. Men indtil videre vil blot brugen af sekvensen fungere og er den mest levedygtige mulighed med den bedste ydeevne og skalerbarhed.
Tilføjelse - MongoDB 3.6 og nyere
Selvom mønsteret vist her er temmelig optimeret på grund af hvordan de andre stadier bliver rullet ind i $lookup
, den har en fejl, nemlig "LEFT JOIN", som normalt er iboende for både $lookup
og handlingerne af populate()
er negeret af "optimal" brug af $unwind
her som ikke bevarer tomme arrays. Du kan tilføje preserveNullAndEmptyArrays
mulighed, men dette negerer "optimeret" sekvensen beskrevet ovenfor og efterlader i det væsentlige alle tre stadier intakte, som normalt ville blive kombineret i optimeringen.
MongoDB 3.6 udvides med en "mere udtryksfuld" form for $lookup
tillader et "sub-pipeline" udtryk. Hvilket ikke kun opfylder målet om at bevare "LEFT JOIN", men stadig tillader en optimal forespørgsel for at reducere returnerede resultater og med en meget forenklet syntaks:
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
$expr
brugt for at matche den erklærede "lokale" værdi med den "fremmede" værdi er faktisk, hvad MongoDB gør "internt" nu med den originale $lookup
syntaks. Ved at udtrykke i denne form kan vi skræddersy den indledende $match
udtryk inden for "sub-pipeline" os selv.
Faktisk kan du som en ægte "aggregeringspipeline" gøre stort set alt, hvad du kan gøre med en aggregeringspipeline inden for dette "sub-pipeline"-udtryk, inklusive "nesting" af niveauerne af $lookup
til andre relaterede samlinger.
Yderligere brug er lidt uden for rækkevidden af, hvad spørgsmålet her stiller, men i forhold til selv "indlejret befolkning" så er det nye brugsmønster for $lookup
tillader dette at være meget det samme, og en "masse" mere kraftfuld i fuld brug.
Arbejdseksempel
Det følgende giver et eksempel med en statisk metode på modellen. Når den statiske metode er implementeret, bliver opkaldet simpelthen:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Eller at forbedre til at være en smule mere moderne bliver endda:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Gør det meget lig .populate()
i struktur, men det gør faktisk joinforbindelsen på serveren i stedet for. For fuldstændighedens skyld kaster brugen her de returnerede data tilbage til mongoose-dokumentforekomster i henhold til både overordnede og underordnede sager.
Det er ret trivielt og nemt at tilpasse eller bare bruge, som det er i de fleste almindelige tilfælde.
NB Brugen af async her er kun for kortheds skyld ved at køre det vedlagte eksempel. Den faktiske implementering er fri for denne afhængighed.
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();
}
)
Eller lidt mere moderne til Node 8.x og nyere med async/await
og ingen yderligere afhængigheder:
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()
}
})()
Og fra MongoDB 3.6 og opefter, selv uden $unwind
og $group
bygning:
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()
}
})()