Z nowoczesną MongoDB większą niż 3.2 możesz użyć $lookup
jako alternatywa dla .populate()
w większości przypadków. Ma to również tę zaletę, że faktycznie wykonuje łączenie „na serwerze” w przeciwieństwie do tego, co .populate()
robi, co w rzeczywistości jest "wieloma zapytaniami" do "emulowania" dołączyć.
Więc .populate()
jest nie tak naprawdę „join” w sensie tego, jak robi to relacyjna baza danych. $lookup
z drugiej strony operator faktycznie wykonuje pracę na serwerze i jest mniej więcej analogiczny do "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"
}
)
Uwaga .collection.name
tutaj w rzeczywistości zwraca się do „ciągu”, który jest rzeczywistą nazwą kolekcji MongoDB przypisaną do modelu. Ponieważ mangusta domyślnie „pluralizuje” nazwy kolekcji i $lookup
potrzebuje rzeczywistej nazwy kolekcji MongoDB jako argumentu (ponieważ jest to operacja serwera), to jest to poręczna sztuczka do użycia w kodzie mongoose, w przeciwieństwie do "twardego kodowania" nazwy kolekcji bezpośrednio.
Chociaż moglibyśmy również użyć $filter
na tablicach do usuwania niechcianych elementów, jest to w rzeczywistości najbardziej wydajna forma ze względu na Optymalizację potoku agregacji dla specjalnego warunku jako $lookup
po którym następuje oba $unwind
i $match
stan.
W rzeczywistości powoduje to połączenie trzech etapów rurociągu w jeden:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
Jest to wysoce optymalne, ponieważ właściwa operacja „najpierw filtruje kolekcję do dołączenia”, a następnie zwraca wyniki i „odwija” tablicę. Stosowane są obie metody, więc wyniki nie przekraczają limitu BSON wynoszącego 16 MB, co jest ograniczeniem, którego klient nie ma.
Jedynym problemem jest to, że pod pewnymi względami wydaje się to „nie intuicyjne”, szczególnie gdy chcesz uzyskać wyniki w postaci tablicy, ale właśnie to jest właśnie to, co $group
jest tutaj, ponieważ rekonstruuje oryginalną formę dokumentu.
Szkoda również, że w tej chwili po prostu nie możemy napisać $lookup
w tej samej ostatecznej składni, której używa serwer. IMHO, to przeoczenie, które należy poprawić. Ale na razie wystarczy użyć sekwencji i jest to najbardziej opłacalna opcja z najlepszą wydajnością i skalowalnością.
Uzupełnienie — MongoDB 3.6 i nowsze
Chociaż pokazany tutaj wzorzec jest dość zoptymalizowany ze względu na sposób, w jaki inne etapy trafiają do $lookup
, ma jedną wadę, ponieważ "LEFT JOIN", która jest zwykle nieodłączna dla obu $lookup
i działania populate()
jest zanegowane przez "optymalne" użycie $unwind
tutaj co nie zachowuje pustych tablic. Możesz dodać preserveNullAndEmptyArrays
opcja, ale to neguje opcję „zoptymalizowaną” opisanej powyżej kolejności i zasadniczo pozostawia nienaruszone wszystkie trzy etapy, które normalnie byłyby połączone w optymalizacji.
MongoDB 3.6 rozwija się z "bardziej wyrazistym" forma $lookup
zezwalając na wyrażenie „podpotok”. Który nie tylko spełnia cel, jakim jest zachowanie „LEFT JOIN”, ale nadal umożliwia optymalne zapytanie w celu zmniejszenia zwracanych wyników i przy znacznie uproszczonej składni:
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
$expr
używane w celu dopasowania zadeklarowanej wartości „lokalnej” do wartości „obcej” jest w rzeczywistości tym, co MongoDB robi teraz „wewnętrznie” z oryginalnym $lookup
składnia. Wyrażając w tej formie możemy dostosować początkowy $match
wyrażenie wewnątrz „podpotoku” sami.
W rzeczywistości, jako prawdziwy „potok agregacji”, możesz zrobić prawie wszystko, co możesz zrobić z potoku agregacji w tym wyrażeniu „podpotoku”, w tym „zagnieżdżanie” poziomów $lookup
do innych powiązanych kolekcji.
Dalsze użycie jest nieco poza zakresem tego, o co pyta to pytanie, ale w odniesieniu nawet do „zagnieżdżonej populacji” nowy wzorzec użycia $lookup
pozwala na to, by było tak samo i "dużo" bardziej wydajny w pełnym wykorzystaniu.
Przykład pracy
Poniżej przedstawiono przykład użycia metody statycznej na modelu. Po zaimplementowaniu tej statycznej metody wywołanie staje się po prostu:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Lub ulepszenie, aby być nieco bardziej nowoczesnym, staje się nawet:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Uczynienie go bardzo podobnym do .populate()
w strukturze, ale zamiast tego wykonuje łączenie na serwerze. Dla kompletności, użycie tutaj rzutuje zwrócone dane z powrotem na instancje dokumentu mangusty w zależności od przypadku nadrzędnego i podrzędnego.
Jest to dość trywialne i łatwe w adaptacji lub po prostu w użyciu, tak jak w większości typowych przypadków.
Uwaga Użycie async jest tutaj tylko dla zwięzłości uruchomienia załączonego przykładu. Rzeczywista implementacja jest wolna od tej zależności.
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();
}
)
Lub trochę bardziej nowoczesny dla Node 8.x i nowszych z async/await
i bez dodatkowych zależności:
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()
}
})()
I od MongoDB 3.6 i nowszych, nawet bez $unwind
i $group
budynek:
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()
}
})()