Với MongoDB hiện đại lớn hơn 3.2, bạn có thể sử dụng $lookup
thay thế .populate()
trong hầu hết các trường hợp. Điều này cũng có lợi thế là thực sự thực hiện tham gia "trên máy chủ" trái ngược với những gì .populate()
thực sự là "nhiều truy vấn" để "mô phỏng" một tham gia.
Vì vậy, .populate()
là không thực sự là một "tham gia" theo nghĩa như thế nào một cơ sở dữ liệu quan hệ hiện nó. Mặt khác, $lookup
toán tử thực sự thực hiện công việc 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) {
}
)
NB Ở .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 nguyên 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ủ), nên đâ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" tên bộ sưu tập trực tiếp .
Mặc dù chúng tôi cũng có thể sử dụng $filter
trên các mảng để loại bỏ các mục không mong muốn, nhưng đâ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 $lookup
theo sau là cả điều kiện $unwind
và $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 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
ở đây, vì nó cấu trúc lại dạng tài liệu ban đầu.
Cũng thật không may là tại thời điểm này, chúng ta không thể thực sự viết $lookup
theo 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ô hình hiển thị ở đây khá tối ưu hóa do cách các giai đoạn khác được đưa vào $lookup
, nhưng nó có một điểm không thành công là "LEFT JOIN" vốn thường có sẵn cho cả hai $lookup
và các hành động của populate()
bị phủ nhận bởi cách sử dụng "tối ưu"$unwind
ở đây không bảo tồn các mảng trống. Bạn có thể thêm preserveNullAndEmptyArrays
tùy chọn, nhưng điều này phủ nhận trình tự "được tối ưu hóa" được mô tả ở trên và về cơ bản giữ nguyên cả ba giai đoạn thường được kết hợp trong tối ưu hóa.
MongoDB 3,6 nở với một "biểu cảm hơn" hình thức $lookup
cho phép một "tiểu đường ống" biểu. Đ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" ] }
}}
]
}}
])
Giá trị $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
cú pháp ban đầu . Bằng cách diễn đạt ở dạng này, chúng ta có thể tự điều chỉnh $match
biểu thức ban đầu trong "đường ống con".
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
các tập hợp liên quan khác.
Việc sử dụng thêm vượt quá phạm vi của những gì câu hỏi ở đây yêu cầu một chút, nhưng liên quan đến thậm chí "quần thể lồng nhau" thì kiểu sử dụng mới của $lookup
cho phép điều này giống nhau và mạnh hơn "rất nhiều" trong việc 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()
cấu trúc, nhưng thay vào đó, nó thực sự thực hiện tham gia trên máy chủ. Để hoàn chỉnh, cách sử dụng ở đây sẽ chuyển dữ liệu được trả về trở lại các cá thể tài liệu mongoose theo cả trường hợp mẹ và con.
Nó khá nhỏ và dễ thích ứng hoặc chỉ sử dụng như đối với hầu hết các trường hợp phổ biến.
NB Việc sử dụng async ở đây chỉ để nói ngắn gọn là 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(
[
(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();
}
)
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);
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()
}
})()
Và từ MongoDB 3.6 trở lên, ngay cả khi không có $unwind
và $group
xây dựng:
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()
}
})()