Med en modern MongoDB större än 3.2 kan du använda $lookup
som ett alternativ till .populate()
i de flesta fallen. Detta har också fördelen av att faktiskt göra joinen "på servern" i motsats till vad .populate()
gör vilket faktiskt är "multiple queries" för att "emulera" en gå med.
Så .populate()
är inte verkligen en "join" i betydelsen hur en relationsdatabas gör det. $lookup
operatör å andra sidan, gör faktiskt jobbet på servern och är mer 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"
}
)
Obs.
.collection.name
här utvärderas faktiskt till "strängen" som är det faktiska namnet på MongoDB-samlingen som tilldelas modellen. Eftersom mongoose "pluraliserar" samlingsnamn som standard och$lookup
behöver det faktiska MongoDB-samlingsnamnet som ett argument (eftersom det är en serveroperation), så är detta ett praktiskt knep att använda i mongoose-kod, i motsats till att "hårdkoda" samlingsnamnet direkt.
Även om vi också skulle kunna använda $filter
på arrayer för att ta bort de oönskade föremålen, är detta faktiskt den mest effektiva formen på grund av aggregation Pipeline Optimization för det speciella tillståndet som $lookup
följt av både en $unwind
och en $match
skick.
Detta resulterar faktiskt i att de tre pipeline-stegen rullas till ett:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
Detta är mycket optimalt eftersom den faktiska operationen "filtrerar samlingen för att gå med först", sedan returnerar den resultaten och "lindar upp" arrayen. Båda metoderna används så att resultaten inte bryter mot BSON-gränsen på 16MB, vilket är en begränsning som klienten inte har.
Det enda problemet är att det verkar "kontraintuitivt" på vissa sätt, särskilt när du vill ha resultaten i en array, men det är vad $group
är för här, eftersom den rekonstruerar till den ursprungliga dokumentformen.
Det är också olyckligt att vi i nuläget helt enkelt inte kan skriva $lookup
i samma slutliga syntax som servern använder. IMHO, detta är ett förbiseende som ska korrigeras. Men för närvarande kommer det att fungera helt enkelt att använda sekvensen och är det mest lönsamma alternativet med bästa prestanda och skalbarhet.
Tillägg - MongoDB 3.6 och senare
Även om mönstret som visas här är ganska optimerat på grund av hur de andra stegen rullas in i $lookup
, den har ett fel genom att "LEFT JOIN" som normalt är inneboende för både $lookup
och åtgärderna för populate()
negeras av "optimal" användning av $unwind
här som inte bevarar tomma arrayer. Du kan lägga till preserveNullAndEmptyArrays
alternativet, men detta förnekar "optimerad" sekvensen som beskrivs ovan och lämnar i huvudsak alla tre stegen intakta som normalt skulle kombineras i optimeringen.
MongoDB 3.6 expanderar med en "mer uttrycksfull" form av $lookup
tillåter ett "sub-pipeline"-uttryck. Vilket inte bara uppfyller målet att behålla "LEFT JOIN" utan fortfarande tillåter en optimal fråga för att minska returnerade resultat och med en mycket förenklad syntax:
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
$expr
som används för att matcha det deklarerade "lokala" värdet med det "utländska" värdet är faktiskt vad MongoDB gör "internt" nu med den ursprungliga $lookup
syntax. Genom att uttrycka i detta formulär kan vi skräddarsy den initiala $match
uttryck inom "sub-pipeline" själva.
Faktum är att som en riktig "aggregationspipeline" kan du göra nästan allt du kan göra med en aggregeringspipeline inom detta "sub-pipeline"-uttryck, inklusive "kapsla" nivåerna för $lookup
till andra relaterade samlingar.
Ytterligare användning är lite utanför räckvidden för vad frågan här ställer, men i förhållande till även "kapslade populationer" är det nya användningsmönstret för $lookup
låter detta vara ungefär detsamma, och en "mängd" kraftfullare i sin fulla användning.
Arbetsexempel
Följande ger ett exempel med en statisk metod på modellen. När den statiska metoden väl är implementerad blir anropet helt enkelt:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Eller att förbättra till att vara lite modernare blir till och med:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Gör det väldigt likt .populate()
i struktur, men det gör faktiskt anslutningen på servern istället. För fullständighetens skull kastar användningen här den returnerade datan tillbaka till mongoose-dokumentinstanser i enlighet med både överordnade och underordnade fall.
Det är ganska trivialt och lätt att anpassa eller bara använda som i de flesta vanliga fall.
OBS Användningen av async här är bara för att köra det bifogade exemplet. Den faktiska implementeringen är fri från detta beroende.
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 lite modernare för Node 8.x och högre med async/await
och inga ytterligare beroenden:
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()
}
})()
Och från MongoDB 3.6 och uppåt, även utan $unwind
och $group
byggnad:
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()
}
})()