Điều cơ bản bạn đã bỏ lỡ ở đây là "đường dẫn" đến trường bạn muốn populate()
thực sự là 'portfolio.formatType'
và không chỉ 'portfolio'
như bạn đã gõ. Tuy nhiên, do sai lầm và cấu trúc đó, bạn có thể có một vài quan niệm sai lầm chung.
Điền thông tin sửa chữa
Việc sửa chữa cơ bản chỉ cần đường dẫn chính xác và bạn không cần model
đối số vì điều này đã được ngụ ý trong lược đồ:
User.findById(req.params.id).populate('portfolio.formatType');
Tuy nhiên, nói chung không phải là một ý tưởng tuyệt vời khi "trộn" cả dữ liệu "nhúng" và dữ liệu "được tham chiếu" trong các mảng và bạn thực sự nên nhúng mọi thứ hoặc đơn giản là tham chiếu mọi thứ. Nói chung, việc giữ một loạt các tham chiếu trong tài liệu nếu bạn có ý định tham chiếu, vì lý do của bạn không nên làm cho tài liệu vượt quá giới hạn 16MB BSON. Và nơi mà dữ liệu của bạn sẽ không bao giờ đạt đến giới hạn đó, nói chung tốt hơn là "nhúng đầy đủ". Đó thực sự là một cuộc thảo luận rộng hơn, nhưng một điều bạn nên biết.
Điểm chung tiếp theo ở đây là populate()
bản thân nó hơi "chiếc mũ cũ", và thực sự không phải là điều "kỳ diệu" mà hầu hết người dùng mới cảm nhận được. Để rõ ràng populate()
KHÔNG PHẢI LÀ THAM GIA và tất cả những gì nó đang làm là thực hiện một truy vấn khác tới máy chủ để trả về các mục "có liên quan", sau đó hợp nhất nội dung đó vào các tài liệu được trả về từ truy vấn trước đó.
$ lookup Alternative
Nếu bạn đang tìm kiếm "tham gia", thì bạn có thể thực sự muốn "nhúng" như đã đề cập trước đó. Đây thực sự là "Cách MongoDB" xử lý "quan hệ" nhưng giữ tất cả dữ liệu "liên quan" cùng nhau trong một tài liệu. Phương tiện khác của "kết hợp" nơi dữ liệu nằm trong các bộ sưu tập riêng biệt là thông qua $lookup
trong các bản phát hành hiện đại.
Điều này sẽ phức tạp hơn một chút do dạng mảng nội dung "hỗn hợp" của bạn, nhưng thường có thể được biểu diễn dưới dạng:
// Aggregation pipeline don't "autocast" from schema
const { Types: { ObjectId } } = require("mongoose");
User.aggregate([
{ "$match": { _id: ObjectId(req.params.id) } },
{ "$lookup": {
"from": FormatType.collection.name,
"localField": "portfolio.formatType",
"foreignField": "_id",
"as": "formats"
}},
{ "$project": {
"name": 1,
"portfolio": {
"$map": {
"input": "$portfolio",
"in": {
"name": "$$this.name",
"formatType": {
"$arrayElemAt": [
"$formats",
{ "$indexOfArray": [ "$formats._id", "$$this.formatType" ] }
]
}
}
}
}
}}
]);
Hoặc với dạng biểu cảm hơn của $lookup
kể từ MongoDB 3.6:
User.aggregate([
{ "$match": { _id: ObjectId(req.params.id) } },
{ "$lookup": {
"from": FormatType.collection.name,
"let": { "portfolio": "$portfolio" },
"as": "portfolio",
"pipeline": [
{ "$match": {
"$expr": {
"$in": [ "$_id", "$$portfolio.formatType" ]
}
}},
{ "$project": {
"_id": {
"$arrayElemAt": [
"$$portfolio._id",
{ "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
]
},
"name": {
"$arrayElemAt": [
"$$portfolio.name",
{ "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
]
},
"formatType": "$$ROOT",
}}
]
}}
]);
Hai cách tiếp cận hoạt động hơi khác nhau, nhưng về cơ bản cả hai đều hoạt động với khái niệm trả về các mục nhập "có liên quan" phù hợp và sau đó "ánh xạ lại" vào nội dung mảng hiện có để hợp nhất với "name"
thuộc tính "nhúng" bên trong mảng. Đó thực sự là sự phức tạp chính mà nếu không thì đây là một phương pháp truy xuất khá đơn giản.
Quá trình này khá giống với quy trình populate()
thực sự thực hiện trên "máy khách" nhưng được thực thi trên "máy chủ". Vì vậy, các so sánh đang sử dụng $indexOfArray
toán tử để tìm nơi kết hợp ObjectId
giá trị là và sau đó trả về một thuộc tính từ mảng tại "chỉ mục" phù hợp đó qua $arrayElemAt
hoạt động.
Sự khác biệt duy nhất là trong phiên bản tương thích MongoDB 3.6, chúng tôi thực hiện "thay thế" đó trong nội dung "nước ngoài" "trước đó" các kết quả đã tham gia được trả lại cho phụ huynh. Trong các bản phát hành trước, chúng tôi trả về toàn bộ mảng ngoại lai phù hợp và sau đó "kết hợp" cả hai để tạo thành một mảng "hợp nhất" số ít bằng cách sử dụng $map
.
Mặc dù ban đầu những điều này có thể trông "phức tạp hơn", nhưng lợi thế lớn ở đây là những điều này tạo thành một "yêu cầu duy nhất" tới máy chủ với một "phản hồi duy nhất" và không phát hành và nhận "nhiều" yêu cầu dưới dạng populate()
làm. Điều này thực sự tiết kiệm rất nhiều chi phí lưu lượng mạng và tăng đáng kể thời gian phản hồi.
Ngoài ra, đây là những "liên kết thực" nên bạn có thể làm được nhiều việc hơn mà không thể đạt được với "nhiều truy vấn". Ví dụ:bạn có thể "sắp xếp" kết quả trên "tham gia" và chỉ trả về kết quả hàng đầu, khi sử dụng populate()
cần phải kéo "tất cả các bậc cha mẹ" vào trước khi nó thậm chí có thể tìm kiếm "con cái" nào để trả về kết quả. Tương tự với các điều kiện "lọc" đối với "tham gia" con cũng vậy.
Có một số chi tiết hơn về điều này trên Truy vấn sau khi điền trong Mongoose về các giới hạn chung và những gì bạn thực sự có thể làm để "tự động hóa" việc tạo các câu lệnh đường ống tổng hợp "phức tạp" như vậy nếu cần.
Trình diễn
Một vấn đề phổ biến khác khi thực hiện các "phép nối" này và hiểu lược đồ được tham chiếu nói chung là mọi người thường hiểu sai khái niệm về vị trí và thời điểm lưu trữ các tham chiếu cũng như cách thức hoạt động của tất cả các tham chiếu. Do đó, các danh sách sau đây đóng vai trò là minh chứng cho cả việc lưu trữ và truy xuất dữ liệu đó.
Trong triển khai Promises gốc cho các bản phát hành NodeJS cũ hơn:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/usertest';
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const formatTypeSchema = new Schema({
name: String
});
const portfolioSchema = new Schema({
name: String,
formatType: { type: Schema.Types.ObjectId, ref: 'FormatType' }
});
const userSchema = new Schema({
name: String,
portfolio: [portfolioSchema]
});
const FormatType = mongoose.model('FormatType', formatTypeSchema);
const User = mongoose.model('User', userSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(function() {
mongoose.connect(uri).then(conn => {
let db = conn.connections[0].db;
return db.command({ buildInfo: 1 }).then(({ version }) => {
version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);
return Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()))
.then(() => FormatType.insertMany(
[ 'A', 'B', 'C' ].map(name => ({ name }))
)
.then(([A, B, C]) => User.insertMany(
[
{
name: 'User 1',
portfolio: [
{ name: 'Port A', formatType: A },
{ name: 'Port B', formatType: B }
]
},
{
name: 'User 2',
portfolio: [
{ name: 'Port C', formatType: C }
]
}
]
))
.then(() => User.find())
.then(users => log({ users }))
.then(() => User.findOne({ name: 'User 1' })
.populate('portfolio.formatType')
)
.then(user1 => log({ user1 }))
.then(() => User.aggregate([
{ "$match": { "name": "User 2" } },
{ "$lookup": {
"from": FormatType.collection.name,
"localField": "portfolio.formatType",
"foreignField": "_id",
"as": "formats"
}},
{ "$project": {
"name": 1,
"portfolio": {
"$map": {
"input": "$portfolio",
"in": {
"name": "$$this.name",
"formatType": {
"$arrayElemAt": [
"$formats",
{ "$indexOfArray": [ "$formats._id", "$$this.formatType" ] }
]
}
}
}
}
}}
]))
.then(user2 => log({ user2 }))
.then(() =>
( version >= 3.6 ) ?
User.aggregate([
{ "$lookup": {
"from": FormatType.collection.name,
"let": { "portfolio": "$portfolio" },
"as": "portfolio",
"pipeline": [
{ "$match": {
"$expr": {
"$in": [ "$_id", "$$portfolio.formatType" ]
}
}},
{ "$project": {
"_id": {
"$arrayElemAt": [
"$$portfolio._id",
{ "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
]
},
"name": {
"$arrayElemAt": [
"$$portfolio.name",
{ "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
]
},
"formatType": "$$ROOT",
}}
]
}}
]).then(users => log({ users })) : ''
);
})
.catch(e => console.error(e))
.then(() => mongoose.disconnect());
})()
Và với async/await
cú pháp cho các bản phát hành NodeJS mới hơn, bao gồm cả dòng LTS v.8.x hiện tại:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/usertest';
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const formatTypeSchema = new Schema({
name: String
});
const portfolioSchema = new Schema({
name: String,
formatType: { type: Schema.Types.ObjectId, ref: 'FormatType' }
});
const userSchema = new Schema({
name: String,
portfolio: [portfolioSchema]
});
const FormatType = mongoose.model('FormatType', formatTypeSchema);
const User = mongoose.model('User', userSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri);
let db = conn.connections[0].db;
let { version } = await db.command({ buildInfo: 1 });
version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);
log(version);
// Clean data
await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));
// Insert some things
let [ A, B, C ] = await FormatType.insertMany(
[ 'A', 'B', 'C' ].map(name => ({ name }))
);
await User.insertMany(
[
{
name: 'User 1',
portfolio: [
{ name: 'Port A', formatType: A },
{ name: 'Port B', formatType: B }
]
},
{
name: 'User 2',
portfolio: [
{ name: 'Port C', formatType: C }
]
}
]
);
// Show plain users
let users = await User.find();
log({ users });
// Get user with populate
let user1 = await User.findOne({ name: 'User 1' })
.populate('portfolio.formatType');
log({ user1 });
// Get user with $lookup
let user2 = await User.aggregate([
{ "$match": { "name": "User 2" } },
{ "$lookup": {
"from": FormatType.collection.name,
"localField": "portfolio.formatType",
"foreignField": "_id",
"as": "formats"
}},
{ "$project": {
"name": 1,
"portfolio": {
"$map": {
"input": "$portfolio",
"in": {
"name": "$$this.name",
"formatType": {
"$arrayElemAt": [
"$formats",
{ "$indexOfArray": [ "$formats._id", "$$this.formatType" ] }
]
}
}
}
}
}}
]);
log({ user2 });
// Expressive $lookup
if ( version >= 3.6 ) {
let users = await User.aggregate([
{ "$lookup": {
"from": FormatType.collection.name,
"let": { "portfolio": "$portfolio" },
"as": "portfolio",
"pipeline": [
{ "$match": {
"$expr": {
"$in": [ "$_id", "$$portfolio.formatType" ]
}
}},
{ "$project": {
"_id": {
"$arrayElemAt": [
"$$portfolio._id",
{ "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
]
},
"name": {
"$arrayElemAt": [
"$$portfolio.name",
{ "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] }
]
},
"formatType": "$$ROOT",
}}
]
}}
]);
log({ users })
}
mongoose.disconnect();
} catch(e) {
console.log(e)
} finally {
process.exit()
}
})()
Danh sách thứ hai nếu được chú thích ở mỗi giai đoạn để giải thích các phần và ít nhất bạn có thể thấy bằng cách so sánh cả hai dạng cú pháp liên quan với nhau như thế nào.
Lưu ý rằng $lookup
"biểu cảm"
ví dụ chỉ chạy khi máy chủ MongoDB được kết nối với thực sự hỗ trợ cú pháp.
Và "đầu ra" cho những người không thể bận tâm khi tự chạy mã:
Mongoose: formattypes.remove({}, {})
Mongoose: users.remove({}, {})
Mongoose: formattypes.insertMany([ { _id: 5b1601d8be9bf225554783f5, name: 'A', __v: 0 }, { _id: 5b1601d8be9bf225554783f6, name: 'B', __v: 0 }, { _id: 5b1601d8be9bf225554783f7, name: 'C', __v: 0 } ], {})
Mongoose: users.insertMany([ { _id: 5b1601d8be9bf225554783f8, name: 'User 1', portfolio: [ { _id: 5b1601d8be9bf225554783fa, name: 'Port A', formatType: 5b1601d8be9bf225554783f5 }, { _id: 5b1601d8be9bf225554783f9, name: 'Port B', formatType: 5b1601d8be9bf225554783f6 } ], __v: 0 }, { _id: 5b1601d8be9bf225554783fb, name: 'User 2', portfolio: [ { _id: 5b1601d8be9bf225554783fc, name: 'Port C', formatType: 5b1601d8be9bf225554783f7 } ], __v: 0 } ], {})
Mongoose: users.find({}, { fields: {} })
{
"users": [
{
"_id": "5b1601d8be9bf225554783f8",
"name": "User 1",
"portfolio": [
{
"_id": "5b1601d8be9bf225554783fa",
"name": "Port A",
"formatType": "5b1601d8be9bf225554783f5"
},
{
"_id": "5b1601d8be9bf225554783f9",
"name": "Port B",
"formatType": "5b1601d8be9bf225554783f6"
}
],
"__v": 0
},
{
"_id": "5b1601d8be9bf225554783fb",
"name": "User 2",
"portfolio": [
{
"_id": "5b1601d8be9bf225554783fc",
"name": "Port C",
"formatType": "5b1601d8be9bf225554783f7"
}
],
"__v": 0
}
]
}
Mongoose: users.findOne({ name: 'User 1' }, { fields: {} })
Mongoose: formattypes.find({ _id: { '$in': [ ObjectId("5b1601d8be9bf225554783f5"), ObjectId("5b1601d8be9bf225554783f6") ] } }, { fields: {} })
{
"user1": {
"_id": "5b1601d8be9bf225554783f8",
"name": "User 1",
"portfolio": [
{
"_id": "5b1601d8be9bf225554783fa",
"name": "Port A",
"formatType": {
"_id": "5b1601d8be9bf225554783f5",
"name": "A",
"__v": 0
}
},
{
"_id": "5b1601d8be9bf225554783f9",
"name": "Port B",
"formatType": {
"_id": "5b1601d8be9bf225554783f6",
"name": "B",
"__v": 0
}
}
],
"__v": 0
}
}
Mongoose: users.aggregate([ { '$match': { name: 'User 2' } }, { '$lookup': { from: 'formattypes', localField: 'portfolio.formatType', foreignField: '_id', as: 'formats' } }, { '$project': { name: 1, portfolio: { '$map': { input: '$portfolio', in: { name: '$$this.name', formatType: { '$arrayElemAt': [ '$formats', { '$indexOfArray': [ '$formats._id', '$$this.formatType' ] } ] } } } } } } ], {})
{
"user2": [
{
"_id": "5b1601d8be9bf225554783fb",
"name": "User 2",
"portfolio": [
{
"name": "Port C",
"formatType": {
"_id": "5b1601d8be9bf225554783f7",
"name": "C",
"__v": 0
}
}
]
}
]
}
Mongoose: users.aggregate([ { '$lookup': { from: 'formattypes', let: { portfolio: '$portfolio' }, as: 'portfolio', pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$portfolio.formatType' ] } } }, { '$project': { _id: { '$arrayElemAt': [ '$$portfolio._id', { '$indexOfArray': [ '$$portfolio.formatType', '$_id' ] } ] }, name: { '$arrayElemAt': [ '$$portfolio.name', { '$indexOfArray': [ '$$portfolio.formatType', '$_id' ] } ] }, formatType: '$$ROOT' } } ] } } ], {})
{
"users": [
{
"_id": "5b1601d8be9bf225554783f8",
"name": "User 1",
"portfolio": [
{
"_id": "5b1601d8be9bf225554783fa",
"name": "Port A",
"formatType": {
"_id": "5b1601d8be9bf225554783f5",
"name": "A",
"__v": 0
}
},
{
"_id": "5b1601d8be9bf225554783f9",
"name": "Port B",
"formatType": {
"_id": "5b1601d8be9bf225554783f6",
"name": "B",
"__v": 0
}
}
],
"__v": 0
},
{
"_id": "5b1601d8be9bf225554783fb",
"name": "User 2",
"portfolio": [
{
"_id": "5b1601d8be9bf225554783fc",
"name": "Port C",
"formatType": {
"_id": "5b1601d8be9bf225554783f7",
"name": "C",
"__v": 0
}
}
],
"__v": 0
}
]
}