Vì vậy, bạn thực sự đang thiếu một số khái niệm ở đây khi bạn yêu cầu "điền" vào một kết quả tổng hợp. Thông thường, đây không phải là những gì bạn thực sự làm, mà là để giải thích các điểm:
-
Kết quả của
aggregate()
không giống nhưModel.find()
hoặc hành động tương tự vì mục đích ở đây là "định hình lại kết quả". Về cơ bản, điều này có nghĩa là mô hình bạn đang sử dụng làm nguồn tổng hợp không còn được coi là mô hình đó trên đầu ra. Điều này thậm chí đúng nếu bạn vẫn duy trì cùng một cấu trúc tài liệu trên đầu ra, nhưng trong trường hợp của bạn, đầu ra rõ ràng khác với tài liệu nguồn.Ở bất kỳ mức độ nào, nó không còn là một bản sao của
Warranty
mô hình mà bạn đang tìm nguồn cung ứng, nhưng chỉ là một đối tượng đơn giản. Chúng tôi có thể giải quyết vấn đề đó khi chúng tôi tiếp xúc sau này. -
Có lẽ điểm chính ở đây là
populate()
có phần "mũ cũ" dù sao. Đây thực sự chỉ là một chức năng tiện lợi được thêm vào Mongoose trong những ngày đầu triển khai. Tất cả những gì nó thực sự làm là thực thi "một truy vấn khác" trên liên quan dữ liệu trong một bộ sưu tập riêng biệt và sau đó kết hợp các kết quả trong bộ nhớ với kết quả đầu ra của bộ sưu tập ban đầu.Vì nhiều lý do, điều đó không thực sự hiệu quả hoặc thậm chí không mong muốn trong hầu hết các trường hợp. Và trái với quan niệm sai lầm phổ biến, điều này KHÔNG thực sự là một "tham gia".
Để có một "tham gia" thực, bạn thực sự sử dụng
$lookup
giai đoạn đường ống tổng hợp, mà MongoDB sử dụng để trả về các mục phù hợp từ một bộ sưu tập khác. Không giống nhưpopulate()
điều này thực sự được thực hiện trong một yêu cầu duy nhất đến máy chủ với một phản hồi duy nhất. Điều này tránh được các chi phí chung của mạng, nói chung là nhanh hơn và vì "tham gia thực" cho phép bạn thực hiện những việcpopulate()
không làm được.
Sử dụng $ lookup thay thế
Rất nhanh chóng phiên bản của điều bị thiếu ở đây là thay vì cố gắng populate()
trong .then()
sau khi kết quả được trả về, những gì bạn làm thay vào đó là thêm $lookup
đến đường ống:
{ "$lookup": {
"from": Account.collection.name,
"localField": "_id",
"foreignField": "_id",
"as": "accounts"
}},
{ "$unwind": "$accounts" },
{ "$project": {
"_id": "$accounts",
"total": 1,
"lineItems": 1
}}
Lưu ý rằng có một hạn chế ở đây trong đó đầu ra của $lookup
là luôn luôn một mảng. Không quan trọng nếu chỉ có một hoặc nhiều mục liên quan được lấy làm đầu ra. Giai đoạn đường ống sẽ tìm kiếm giá trị của "localField"
từ tài liệu hiện tại được trình bày và sử dụng tài liệu đó để khớp với các giá trị trong "foreignField"
được chỉ định. Trong trường hợp này, đó là _id
từ tập hợp $group
nhắm mục tiêu đến _id
của bộ sưu tập nước ngoài.
Vì đầu ra luôn là một mảng như đã đề cập, cách hiệu quả nhất để làm việc với điều này cho trường hợp này là chỉ cần thêm $unwind
giai đoạn trực tiếp sau $lookup
. Tất cả điều này sẽ thực hiện nó trả về một tài liệu mới cho mỗi mục được trả về trong mảng đích và trong trường hợp này, bạn mong đợi nó là một. Trong trường hợp _id
không phù hợp trong bộ sưu tập nước ngoài, kết quả không có kết quả phù hợp sẽ bị xóa.
Một lưu ý nhỏ, đây thực sự là một mẫu được tối ưu hóa như được mô tả trong $ lookup + $ unwind Coalescence
trong tài liệu cốt lõi. Một điều đặc biệt xảy ra ở đây khi $unwind
hướng dẫn thực sự được hợp nhất vào $lookup
hoạt động một cách hiệu quả. Bạn có thể đọc thêm về điều đó ở đó.
Sử dụng điền
Từ nội dung trên, bạn sẽ có thể hiểu về cơ bản tại sao populate()
đây là điều sai lầm để làm. Ngoài thực tế cơ bản là đầu ra không còn bao gồm Warranty
các đối tượng mô hình, mô hình đó thực sự chỉ biết về các mục nước ngoài được mô tả trên _accountId
thuộc tính không tồn tại trong đầu ra.
Bây giờ bạn có thể thực sự xác định một mô hình có thể được sử dụng để truyền các đối tượng đầu ra một cách rõ ràng thành một kiểu đầu ra xác định. Một minh chứng ngắn về một sẽ liên quan đến việc thêm mã vào ứng dụng của bạn cho điều này như:
// Special models
const outputSchema = new Schema({
_id: { type: Schema.Types.ObjectId, ref: "Account" },
total: Number,
lineItems: [{ address: String }]
});
const Output = mongoose.model('Output', outputSchema, 'dontuseme');
Output
mới này sau đó, mô hình có thể được sử dụng để "truyền" các đối tượng JavaScript đơn giản kết quả vào Mongoose Documents để các phương thức như Model.populate()
thực sự có thể được gọi là:
// excerpt
result2 = result2.map(r => new Output(r)); // Cast to Output Mongoose Documents
// Call populate on the list of documents
result2 = await Output.populate(result2, { path: '_id' })
log(result2);
Kể từ khi Output
có một lược đồ được xác định nhận biết về "tham chiếu" trên _id
trường của nó tài liệu Model.populate()
nhận thức được những gì nó cần phải làm và trả lại các mục.
Hãy cẩn thận vì điều này thực sự tạo ra một truy vấn khác. tức là:
Mongoose: warranties.aggregate([ { '$match': { payStatus: 'Invoiced Next Billing Cycle' } }, { '$group': { _id: '$_accountId', total: { '$sum': '$warrantyFee' }, lineItems: { '$push': { _id: '$_id', address: { '$trim': { input: { '$reduce': { input: { '$objectToArray': '$address' }, initialValue: '', in: { '$concat': [ '$$value', ' ', [Object] ] } } }, chars: ' ' } } } } } } ], {})
Mongoose: accounts.find({ _id: { '$in': [ ObjectId("5bf4b591a06509544b8cf75c"), ObjectId("5bf4b591a06509544b8cf75b") ] } }, { projection: {} })
Trong đó dòng đầu tiên là kết quả tổng hợp và sau đó bạn liên hệ lại với máy chủ để trả về Account
có liên quan mục mô hình.
Tóm tắt
Vì vậy, đó là các tùy chọn của bạn, nhưng cần khá rõ ràng rằng cách tiếp cận hiện đại cho việc này là thay vào đó sử dụng $lookup
và nhận được một "tham gia" thực sự không phải là những gì populate()
đang thực sự làm.
Bao gồm một danh sách như một minh chứng đầy đủ về cách mỗi phương pháp này thực sự hoạt động trong thực tế. Một số giấy phép nghệ thuật được lấy ở đây, vì vậy các mô hình được đại diện có thể không chính xác giống như những gì bạn có, nhưng có đủ để chứng minh các khái niệm cơ bản theo cách có thể tái tạo:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/joindemo';
const opts = { useNewUrlParser: true };
// Sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
// Schema defs
const warrantySchema = new Schema({
address: {
street: String,
city: String,
state: String,
zip: Number
},
warrantyFee: Number,
_accountId: { type: Schema.Types.ObjectId, ref: "Account" },
payStatus: String
});
const accountSchema = new Schema({
name: String,
contactName: String,
contactEmail: String
});
// Special models
const outputSchema = new Schema({
_id: { type: Schema.Types.ObjectId, ref: "Account" },
total: Number,
lineItems: [{ address: String }]
});
const Output = mongoose.model('Output', outputSchema, 'dontuseme');
const Warranty = mongoose.model('Warranty', warrantySchema);
const Account = mongoose.model('Account', accountSchema);
// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
// main
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// clean models
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
)
// set up data
let [first, second, third] = await Account.insertMany(
[
['First Account', 'First Person', '[email protected]'],
['Second Account', 'Second Person', '[email protected]'],
['Third Account', 'Third Person', '[email protected]']
].map(([name, contactName, contactEmail]) =>
({ name, contactName, contactEmail })
)
);
await Warranty.insertMany(
[
{
address: {
street: '1 Some street',
city: 'Somewhere',
state: 'TX',
zip: 1234
},
warrantyFee: 100,
_accountId: first,
payStatus: 'Invoiced Next Billing Cycle'
},
{
address: {
street: '2 Other street',
city: 'Elsewhere',
state: 'CA',
zip: 5678
},
warrantyFee: 100,
_accountId: first,
payStatus: 'Invoiced Next Billing Cycle'
},
{
address: {
street: '3 Other street',
city: 'Elsewhere',
state: 'NY',
zip: 1928
},
warrantyFee: 100,
_accountId: first,
payStatus: 'Invoiced Already'
},
{
address: {
street: '21 Jump street',
city: 'Anywhere',
state: 'NY',
zip: 5432
},
warrantyFee: 100,
_accountId: second,
payStatus: 'Invoiced Next Billing Cycle'
}
]
);
// Aggregate $lookup
let result1 = await Warranty.aggregate([
{ "$match": {
"payStatus": "Invoiced Next Billing Cycle"
}},
{ "$group": {
"_id": "$_accountId",
"total": { "$sum": "$warrantyFee" },
"lineItems": {
"$push": {
"_id": "$_id",
"address": {
"$trim": {
"input": {
"$reduce": {
"input": { "$objectToArray": "$address" },
"initialValue": "",
"in": {
"$concat": [ "$$value", " ", { "$toString": "$$this.v" } ] }
}
},
"chars": " "
}
}
}
}
}},
{ "$lookup": {
"from": Account.collection.name,
"localField": "_id",
"foreignField": "_id",
"as": "accounts"
}},
{ "$unwind": "$accounts" },
{ "$project": {
"_id": "$accounts",
"total": 1,
"lineItems": 1
}}
])
log(result1);
// Convert and populate
let result2 = await Warranty.aggregate([
{ "$match": {
"payStatus": "Invoiced Next Billing Cycle"
}},
{ "$group": {
"_id": "$_accountId",
"total": { "$sum": "$warrantyFee" },
"lineItems": {
"$push": {
"_id": "$_id",
"address": {
"$trim": {
"input": {
"$reduce": {
"input": { "$objectToArray": "$address" },
"initialValue": "",
"in": {
"$concat": [ "$$value", " ", { "$toString": "$$this.v" } ] }
}
},
"chars": " "
}
}
}
}
}}
]);
result2 = result2.map(r => new Output(r));
result2 = await Output.populate(result2, { path: '_id' })
log(result2);
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
Và đầu ra đầy đủ:
Mongoose: dontuseme.deleteMany({}, {})
Mongoose: warranties.deleteMany({}, {})
Mongoose: accounts.deleteMany({}, {})
Mongoose: accounts.insertMany([ { _id: 5bf4b591a06509544b8cf75b, name: 'First Account', contactName: 'First Person', contactEmail: '[email protected]', __v: 0 }, { _id: 5bf4b591a06509544b8cf75c, name: 'Second Account', contactName: 'Second Person', contactEmail: '[email protected]', __v: 0 }, { _id: 5bf4b591a06509544b8cf75d, name: 'Third Account', contactName: 'Third Person', contactEmail: '[email protected]', __v: 0 } ], {})
Mongoose: warranties.insertMany([ { _id: 5bf4b591a06509544b8cf75e, address: { street: '1 Some street', city: 'Somewhere', state: 'TX', zip: 1234 }, warrantyFee: 100, _accountId: 5bf4b591a06509544b8cf75b, payStatus: 'Invoiced Next Billing Cycle', __v: 0 }, { _id: 5bf4b591a06509544b8cf75f, address: { street: '2 Other street', city: 'Elsewhere', state: 'CA', zip: 5678 }, warrantyFee: 100, _accountId: 5bf4b591a06509544b8cf75b, payStatus: 'Invoiced Next Billing Cycle', __v: 0 }, { _id: 5bf4b591a06509544b8cf760, address: { street: '3 Other street', city: 'Elsewhere', state: 'NY', zip: 1928 }, warrantyFee: 100, _accountId: 5bf4b591a06509544b8cf75b, payStatus: 'Invoiced Already', __v: 0 }, { _id: 5bf4b591a06509544b8cf761, address: { street: '21 Jump street', city: 'Anywhere', state: 'NY', zip: 5432 }, warrantyFee: 100, _accountId: 5bf4b591a06509544b8cf75c, payStatus: 'Invoiced Next Billing Cycle', __v: 0 } ], {})
Mongoose: warranties.aggregate([ { '$match': { payStatus: 'Invoiced Next Billing Cycle' } }, { '$group': { _id: '$_accountId', total: { '$sum': '$warrantyFee' }, lineItems: { '$push': { _id: '$_id', address: { '$trim': { input: { '$reduce': { input: { '$objectToArray': '$address' }, initialValue: '', in: { '$concat': [ '$$value', ' ', [Object] ] } } }, chars: ' ' } } } } } }, { '$lookup': { from: 'accounts', localField: '_id', foreignField: '_id', as: 'accounts' } }, { '$unwind': '$accounts' }, { '$project': { _id: '$accounts', total: 1, lineItems: 1 } } ], {})
[
{
"total": 100,
"lineItems": [
{
"_id": "5bf4b591a06509544b8cf761",
"address": "21 Jump street Anywhere NY 5432"
}
],
"_id": {
"_id": "5bf4b591a06509544b8cf75c",
"name": "Second Account",
"contactName": "Second Person",
"contactEmail": "[email protected]",
"__v": 0
}
},
{
"total": 200,
"lineItems": [
{
"_id": "5bf4b591a06509544b8cf75e",
"address": "1 Some street Somewhere TX 1234"
},
{
"_id": "5bf4b591a06509544b8cf75f",
"address": "2 Other street Elsewhere CA 5678"
}
],
"_id": {
"_id": "5bf4b591a06509544b8cf75b",
"name": "First Account",
"contactName": "First Person",
"contactEmail": "[email protected]",
"__v": 0
}
}
]
Mongoose: warranties.aggregate([ { '$match': { payStatus: 'Invoiced Next Billing Cycle' } }, { '$group': { _id: '$_accountId', total: { '$sum': '$warrantyFee' }, lineItems: { '$push': { _id: '$_id', address: { '$trim': { input: { '$reduce': { input: { '$objectToArray': '$address' }, initialValue: '', in: { '$concat': [ '$$value', ' ', [Object] ] } } }, chars: ' ' } } } } } } ], {})
Mongoose: accounts.find({ _id: { '$in': [ ObjectId("5bf4b591a06509544b8cf75c"), ObjectId("5bf4b591a06509544b8cf75b") ] } }, { projection: {} })
[
{
"_id": {
"_id": "5bf4b591a06509544b8cf75c",
"name": "Second Account",
"contactName": "Second Person",
"contactEmail": "[email protected]",
"__v": 0
},
"total": 100,
"lineItems": [
{
"_id": "5bf4b591a06509544b8cf761",
"address": "21 Jump street Anywhere NY 5432"
}
]
},
{
"_id": {
"_id": "5bf4b591a06509544b8cf75b",
"name": "First Account",
"contactName": "First Person",
"contactEmail": "[email protected]",
"__v": 0
},
"total": 200,
"lineItems": [
{
"_id": "5bf4b591a06509544b8cf75e",
"address": "1 Some street Somewhere TX 1234"
},
{
"_id": "5bf4b591a06509544b8cf75f",
"address": "2 Other street Elsewhere CA 5678"
}
]
}
]