Dengan MongoDB modern yang lebih besar dari 3,2 Anda dapat menggunakan $lookup
sebagai pengganti .populate()
umumnya. Ini juga memiliki keuntungan untuk benar-benar melakukan penggabungan "di server" sebagai lawan dari .populate()
apakah yang sebenarnya "beberapa kueri" untuk "ditiru" bergabung.
Jadi .populate()
adalah tidak benar-benar "bergabung" dalam arti bagaimana database relasional melakukannya. $lookup
operator di sisi lain, benar-benar bekerja di server, dan kurang lebih analog dengan "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.
.collection.name
di sini sebenarnya mengevaluasi ke "string" yang merupakan nama sebenarnya dari koleksi MongoDB seperti yang ditetapkan untuk model. Sejak luwak "memperbanyak" nama koleksi secara default dan$lookup
membutuhkan nama koleksi MongoDB yang sebenarnya sebagai argumen (karena ini adalah operasi server), maka ini adalah trik praktis untuk digunakan dalam kode luwak, sebagai lawan dari "pengkodean keras" nama koleksi secara langsung.
Sementara kita juga bisa menggunakan $filter
pada array untuk menghapus item yang tidak diinginkan, ini sebenarnya adalah bentuk yang paling efisien karena Agregasi Pipeline Optimization untuk kondisi khusus sebagai $lookup
diikuti oleh $unwind
dan $match
kondisi.
Ini sebenarnya menghasilkan tiga tahap saluran yang digabung menjadi satu:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
Ini sangat optimal karena operasi sebenarnya "memfilter koleksi untuk bergabung terlebih dahulu", lalu mengembalikan hasil dan "melepas" array. Kedua metode digunakan agar hasilnya tidak melanggar batas BSON sebesar 16 MB, yang merupakan batasan yang tidak dimiliki klien.
Satu-satunya masalah adalah tampaknya "kontra-intuitif" dalam beberapa hal, terutama ketika Anda menginginkan hasil dalam array, tetapi itulah yang $group
adalah untuk di sini, karena direkonstruksi ke bentuk dokumen asli.
Sayangnya, saat ini kami tidak dapat benar-benar menulis $lookup
dalam sintaks akhirnya yang sama yang digunakan server. IMHO, ini adalah kekeliruan yang harus diperbaiki. Namun untuk saat ini, hanya menggunakan urutan akan berhasil dan merupakan opsi yang paling memungkinkan dengan kinerja dan skalabilitas terbaik.
Addendum - MongoDB 3.6 dan lebih tinggi
Meskipun pola yang ditampilkan di sini cukup dioptimalkan karena bagaimana tahapan lainnya dimasukkan ke dalam $lookup
, ia memiliki satu kegagalan dalam "LEFT JOIN" yang biasanya melekat pada keduanya $lookup
dan tindakan populate()
dinegasikan oleh "optimal" penggunaan $unwind
di sini yang tidak mempertahankan array kosong. Anda dapat menambahkan preserveNullAndEmptyArrays
opsi, tetapi ini meniadakan "dioptimalkan" urutan yang dijelaskan di atas dan pada dasarnya membiarkan ketiga tahap tetap utuh yang biasanya digabungkan dalam pengoptimalan.
MongoDB 3.6 berkembang dengan "lebih ekspresif" bentuk $lookup
memungkinkan ekspresi "sub-pipa". Yang tidak hanya memenuhi tujuan mempertahankan "LEFT JOIN" tetapi masih memungkinkan kueri yang optimal untuk mengurangi hasil yang dikembalikan dan dengan sintaks yang jauh lebih sederhana:
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
$expr
digunakan untuk mencocokkan nilai "lokal" yang dideklarasikan dengan nilai "asing" sebenarnya adalah apa yang MongoDB lakukan "secara internal" sekarang dengan $lookup
asli sintaksis. Dengan mengekspresikan dalam formulir ini kita dapat menyesuaikan $match
awal ekspresi dalam "sub-pipa" itu sendiri.
Faktanya, sebagai "pipa agregasi" sejati, Anda dapat melakukan apa saja yang dapat Anda lakukan dengan pipa agregasi dalam ekspresi "sub-pipa" ini, termasuk "menyarangkan" level $lookup
ke koleksi terkait lainnya.
Penggunaan lebih lanjut sedikit di luar cakupan pertanyaan di sini, tetapi dalam kaitannya dengan bahkan "populasi bersarang" maka pola penggunaan baru $lookup
memungkinkan ini menjadi hampir sama, dan "lot" lebih kuat dalam penggunaan penuhnya.
Contoh Kerja
Berikut ini adalah contoh penggunaan metode statis pada model. Setelah metode statis diimplementasikan, panggilan menjadi:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Atau ditingkatkan menjadi sedikit lebih modern bahkan menjadi:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Membuatnya sangat mirip dengan .populate()
dalam struktur, tetapi sebenarnya melakukan penggabungan di server sebagai gantinya. Untuk kelengkapan, penggunaan di sini mengembalikan data yang dikembalikan ke instance dokumen luwak sesuai dengan kasus induk dan anak.
Ini cukup sepele dan mudah untuk diadaptasi atau digunakan sebagaimana adanya untuk sebagian besar kasus umum.
N.B Penggunaan async di sini hanya untuk singkatnya menjalankan contoh terlampir. Implementasi sebenarnya bebas dari ketergantungan ini.
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();
}
)
Atau sedikit lebih modern untuk Node 8.x ke atas dengan async/await
dan tidak ada dependensi tambahan:
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()
}
})()
Dan dari MongoDB 3.6 ke atas, bahkan tanpa $unwind
dan $group
bangunan:
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()
}
})()