Với MongoDB hiện đại lớn hơn 3.2, bạn có thể sử dụng $lookup
thay thế cho .populate()
trong hầu hết các trường hợp. Điều này cũng có lợi thế khi thực sự thực hiện tham gia "trên máy chủ" trái ngược với .populate()
thực sự là "nhiều truy vấn" để "mô phỏng" một tham gia.
Vì vậy, .populate()
không thực sự là một "tham gia" theo nghĩa là cách một cơ sở dữ liệu quan hệ thực hiện nó. $lookup
mặt khác, toán tử thực sự hoạt động trên máy chủ và ít nhiều tương tự với "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
ở đây thực sự đánh giá "chuỗi" là tên thực của bộ sưu tập MongoDB như được gán cho mô hình. Vì mongoose "đa phương hóa" tên bộ sưu tập theo mặc định và$lookup
cần tên bộ sưu tập MongoDB thực tế làm đối số (vì đó là hoạt động của máy chủ), thì đây là một thủ thuật hữu ích để sử dụng trong mã mongoose, trái ngược với "mã hóa cứng" trực tiếp tên bộ sưu tập.
Trong khi đó, chúng tôi cũng có thể sử dụng $filter
trên mảng để loại bỏ các mục không mong muốn, đây thực sự là biểu mẫu hiệu quả nhất do Tối ưu hóa đường ống tổng hợp cho điều kiện đặc biệt là $lookup
theo sau là cả $unwind
và một $match
điều kiện.
Điều này thực sự dẫn đến ba giai đoạn đường ống được cuộn thành một:
{ "$lookup" : {
"from" : "itemtags",
"as" : "tags",
"localField" : "tags",
"foreignField" : "_id",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
},
"matching" : {
"tagName" : {
"$in" : [
"funny",
"politics"
]
}
}
}}
Điều này rất tối ưu vì hoạt động thực tế "lọc bộ sưu tập để tham gia trước", sau đó nó trả về kết quả và "giải nén" mảng. Cả hai phương pháp đều được sử dụng để kết quả không phá vỡ giới hạn BSON là 16MB, đây là một hạn chế mà máy khách không có.
Vấn đề duy nhất là nó có vẻ "phản trực quan" theo một số cách, đặc biệt khi bạn muốn kết quả trong một mảng, nhưng đó là những gì $group
là ở đây, vì nó tái cấu trúc về dạng tài liệu ban đầu.
Cũng rất tiếc là tại thời điểm này, chúng tôi không thể thực sự viết $lookup
trong cùng một cú pháp cuối cùng mà máy chủ sử dụng. IMHO, đây là một sơ suất cần được sửa chữa. Nhưng hiện tại, chỉ cần sử dụng trình tự sẽ hoạt động và là lựa chọn khả thi nhất với hiệu suất và khả năng mở rộng tốt nhất.
Phụ lục - MongoDB 3.6 trở lên
Mặc dù mẫu được hiển thị ở đây là khá tối ưu hóa do cách các giai đoạn khác được đưa vào $lookup
, nó không có một lỗi nào đó là "LEFT JOIN" thường có trong cả $lookup
và các hành động của populate()
bị phủ định bởi "tối ưu" sử dụng $unwind
ở đây không bảo tồn các mảng trống. Bạn có thể thêm preserveNullAndEmptyArrays
nhưng điều này phủ nhận tùy chọn "được tối ưu hóa" trình tự được mô tả ở trên và về cơ bản giữ nguyên cả ba giai đoạn mà thường sẽ được kết hợp trong tối ưu hóa.
MongoDB 3.6 mở rộng với "biểu cảm hơn" dạng $lookup
cho phép một biểu thức "đường ống con". Điều này không chỉ đáp ứng mục tiêu giữ lại "LEFT JOIN" mà vẫn cho phép truy vấn tối ưu để giảm kết quả trả về và với cú pháp đơn giản hơn nhiều:
Item.aggregate([
{ "$lookup": {
"from": ItemTags.collection.name,
"let": { "tags": "$tags" },
"pipeline": [
{ "$match": {
"tags": { "$in": [ "politics", "funny" ] },
"$expr": { "$in": [ "$_id", "$$tags" ] }
}}
]
}}
])
$expr
được sử dụng để khớp giá trị "cục bộ" đã khai báo với giá trị "nước ngoài" thực sự là những gì MongoDB thực hiện "nội bộ" bây giờ với $lookup
ban đầu cú pháp. Bằng cách thể hiện trong biểu mẫu này, chúng tôi có thể điều chỉnh $match
ban đầu biểu thức bên trong "đường dẫn phụ" của chính chúng ta.
Trên thực tế, với tư cách là một "đường ống tổng hợp" thực sự, bạn có thể làm bất cứ điều gì bạn có thể làm với đường ống tổng hợp trong biểu thức "đường ống con" này, bao gồm cả việc "lồng" các cấp độ của $lookup
tới các bộ sưu tập khác có liên quan.
Việc sử dụng thêm nằm ngoài phạm vi của những gì câu hỏi ở đây yêu cầu, nhưng liên quan đến thậm chí "dân số lồng nhau" thì kiểu sử dụng mới của $lookup
cho phép điều này giống nhau nhiều và một "lot" mạnh mẽ hơn khi sử dụng đầy đủ.
Ví dụ làm việc
Sau đây là một ví dụ sử dụng phương thức tĩnh trên mô hình. Khi phương thức tĩnh đó được triển khai, cuộc gọi chỉ đơn giản trở thành:
Item.lookup(
{
path: 'tags',
query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
},
callback
)
Hoặc nâng cao để hiện đại hơn một chút thậm chí trở thành:
let results = await Item.lookup({
path: 'tags',
query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
})
Làm cho nó rất giống với .populate()
trong cấu trúc, nhưng nó thực sự thực hiện tham gia trên máy chủ. Để hoàn thiện, cách sử dụng ở đây sẽ chuyển dữ liệu được trả về trở lại các phiên bản tài liệu mongoose theo cả trường hợp mẹ và con.
Nó khá nhỏ và dễ điều chỉnh hoặc chỉ sử dụng như đối với hầu hết các trường hợp phổ biến.
N.B Việc sử dụng async ở đây chỉ để nói ngắn gọn về việc chạy ví dụ kèm theo. Việc triển khai thực tế không có sự phụ thuộc này.
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();
}
)
Hoặc hiện đại hơn một chút cho Node 8.x trở lên với async/await
và không có phụ thuộc bổ sung:
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()
}
})()
Và từ MongoDB 3.6 trở lên, ngay cả khi không có $unwind
và $group
tòa nhà:
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()
}
})()