Nowoczesna baza danych MongoDB w wersji wyższej niż 3.2 może służyć $lookup
jako alternatywa .populate()
w większości przypadków. Ma to również tę zaletę, że faktycznie wykonuje łączenie „na serwerze”, w przeciwieństwie do tego .populate()
, co faktycznie polega na „wielokrotnym zapytaniu” w celu „emulacji” złączenia.
Więc .populate()
to nie naprawdę „join” w sensie jak relacyjna baza danych to robi. Z $lookup
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) {
}
)
Uwaga : .collection.name
tutaj w rzeczywistości jest wynikiem „ciągu”, czyli rzeczywistej nazwy kolekcji MongoDB przypisanej do modelu. Ponieważ mongoose domyślnie „ $lookup
tworzy liczbę mnogą” nazw kolekcji i wymaga rzeczywistej nazwy kolekcji MongoDB jako argumentu (ponieważ jest to operacja na serwerze), jest to przydatna sztuczka do użycia w kodzie mangusty, w przeciwieństwie do „twardego kodowania” bezpośrednio nazwy kolekcji .
Chociaż moglibyśmy również użyć $filter
w tablicach do usuwania niechcianych elementów, jest to w rzeczywistości najbardziej wydajna forma ze względu na optymalizację potoku agregacji dla specjalnego warunku, $lookup
po którym następuje zarówno warunek, jak $unwind
i $match
warunek.
W rzeczywistości powoduje to połączenie trzech etapów potoku w jeden:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
Jest to wysoce optymalne, ponieważ faktyczna operacja „najpierw filtruje zbiór, aby dołączyć”, a następnie zwraca wyniki i „rozwija” tablicę. Obie metody są stosowane, więc wyniki nie przekraczają limitu BSON wynoszącego 16 MB, co jest ograniczeniem, którego klient nie ma.
Jedynym problemem jest to, że wydaje się to „sprzeczne z intuicją” pod pewnymi względami, szczególnie gdy chcesz uzyskać wyniki w tablicy, ale do tego właśnie $group
służy tutaj, ponieważ rekonstruuje do pierwotnej postaci dokumentu.
Szkoda również, że w tej chwili po prostu nie możemy pisać $lookup
w tej samej ostatecznej składni, której używa serwer. IMHO, to jest przeoczenie, które należy naprawić. Ale na razie zwykłe użycie sekwencji zadziała i jest najbardziej realną opcją o najlepszej wydajności i skalowalności.
Dodatek - MongoDB 3.6 i nowsze
Chociaż pokazany tutaj wzorzec jest dość zoptymalizowany ze względu na to, jak inne etapy są wtaczane do $lookup
, ma jeden błąd, ponieważ „LEFT JOIN”, który jest zwykle nieodłączny dla obu, $lookup
a działania populate()
jest negowane przez „optymalne” użycie $unwind
tutaj, który nie zachowuje pustych tablic. Możesz dodać preserveNullAndEmptyArrays
opcję, ale to neguje „zoptymalizowaną” sekwencję opisaną powyżej i zasadniczo pozostawia wszystkie trzy etapy nienaruszone, które normalnie byłyby połączone w optymalizacji.
MongoDB 3.6 rozszerza się o „bardziej wyrazistą” formę $lookup
zezwalania na wyrażenie „pod-potok”. Co nie tylko spełnia cel polegający na zachowaniu „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 deklarowana „lokalny” wartość z wartością „obcego” jest rzeczywiście to, co robi MongoDB „wewnętrznie” teraz z oryginalnego $lookup
składni. Wyrażając w tej formie możemy sami dostosować początkowe $match
wyrażenie w „pod-potoku”.
W rzeczywistości, jako prawdziwy „potok agregacji”, możesz zrobić prawie wszystko, co możesz zrobić z potokiem agregacji w ramach tego wyrażenia „potok podrzędny”, w tym „zagnieżdżać” poziomy w $lookup
innych powiązanych kolekcjach.
Dalsze użycie wykracza nieco poza zakres tego, o co chodzi w tym pytaniu, ale w odniesieniu do nawet „zagnieżdżonej populacji” nowy wzorzec użycia $lookup
pozwala na to, aby było to prawie takie samo i „dużo” potężniejsze w pełnym wykorzystaniu.
Przykład roboczy
Poniżej przedstawiono przykład użycia metody statycznej w modelu. Po zaimplementowaniu tej statycznej metody wywołanie staje się po prostu:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Lub ulepszanie, aby być nieco bardziej nowoczesnym, staje się nawet:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
.populate()
Robi to bardzo podobnie do struktury, ale zamiast tego wykonuje łączenie na serwerze. Aby uzyskać kompletność, użycie tutaj rzutuje zwrócone dane z powrotem do instancji dokumentu mangusty zgodnie z przypadkami nadrzędnymi i podrzędnymi.
Jest dość trywialny i łatwy w adaptacji lub po prostu w użyciu, jak w większości typowych przypadków.
Uwaga : użycie async w tym miejscu służy jedynie zwięzłemu uruchomieniu 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(
[
(callback) => async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
(callback) =>
async.waterfall(
[
(callback) =>
ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],
callback),
(tags, callback) =>
Item.create({ "title": "Something","description": "An item",
"tags": tags },callback)
],
callback
),
(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ę nowocześniejszy dla Node 8.x i nowszych async/await
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);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
const tags = await ItemTag.create(
["movies", "funny"].map(tagName =>({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
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()
}
})()
A od MongoDB 3.6 i nowszych, nawet bez $unwind
i $group
budynku:
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);
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);
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
const tags = await ItemTag.insertMany(
["movies", "funny"].map(tagName => ({ tagName }))
);
const item = await Item.create({
"title": "Something",
"description": "An item",
tags
});
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()
}
})()