Jadi Anda sebenarnya kehilangan beberapa konsep di sini ketika Anda meminta untuk "mengisi" pada hasil agregasi. Biasanya ini bukan apa yang sebenarnya Anda lakukan, tetapi untuk menjelaskan poin-poinnya:
-
Output dari
aggregate()
tidak sepertiModel.find()
atau tindakan serupa karena tujuannya di sini adalah untuk "membentuk kembali hasil". Ini pada dasarnya berarti bahwa model yang Anda gunakan sebagai sumber agregasi tidak lagi dianggap sebagai model pada output. Ini bahkan benar jika Anda masih mempertahankan struktur dokumen yang sama persis pada keluaran, tetapi dalam kasus Anda, keluarannya jelas berbeda dengan dokumen sumber.Bagaimanapun itu bukan lagi contoh
Warranty
model yang Anda sumber, tetapi hanya objek biasa. Kita bisa mengatasinya saat kita membahasnya nanti. -
Mungkin poin utama di sini adalah
populate()
agak "topi tua" omong-omong. Ini benar-benar hanya fungsi kenyamanan yang ditambahkan ke Mongoose pada hari-hari awal implementasi. Yang benar-benar dilakukannya hanyalah menjalankan "permintaan lain" pada terkait data dalam koleksi terpisah, lalu menggabungkan hasilnya di memori ke output koleksi asli.Untuk banyak alasan, itu tidak terlalu efisien atau bahkan diinginkan dalam banyak kasus. Dan bertentangan dengan kesalahpahaman populer, ini TIDAK sebenarnya "bergabung".
Untuk "bergabung" yang sebenarnya, Anda sebenarnya menggunakan
$lookup
tahap pipa agregasi, yang digunakan MongoDB untuk mengembalikan item yang cocok dari koleksi lain. Tidak sepertipopulate()
ini sebenarnya dilakukan dalam satu permintaan ke server dengan satu respons. Ini menghindari overhead jaringan, umumnya lebih cepat dan sebagai "gabungan nyata" memungkinkan Anda melakukan hal-hal yangpopulate()
tidak bisa.
Gunakan $lookup sebagai gantinya
Sangat cepat versi dari apa yang hilang di sini adalah alih-alih mencoba populate()
di .then()
setelah hasilnya dikembalikan, yang Anda lakukan adalah menambahkan $lookup
ke saluran:
{ "$lookup": {
"from": Account.collection.name,
"localField": "_id",
"foreignField": "_id",
"as": "accounts"
}},
{ "$unwind": "$accounts" },
{ "$project": {
"_id": "$accounts",
"total": 1,
"lineItems": 1
}}
Perhatikan bahwa ada batasan di sini bahwa output dari $lookup
adalah selalu sebuah array. Tidak masalah jika hanya ada satu item terkait atau banyak yang akan diambil sebagai output. Tahap pipeline akan mencari nilai "localField"
dari dokumen saat ini yang disajikan dan gunakan itu untuk mencocokkan nilai di "foreignField"
ditentukan. Dalam hal ini adalah _id
dari agregasi $group
targetkan ke _id
koleksi asing.
Karena outputnya selalu berupa array seperti yang disebutkan, cara paling efisien untuk bekerja dengan ini untuk contoh ini adalah dengan menambahkan $unwind
panggung langsung mengikuti $lookup
. Semua ini akan mengembalikan dokumen baru untuk setiap item yang dikembalikan dalam array target, dan dalam hal ini Anda mengharapkannya menjadi satu. Dalam hal _id
tidak cocok dalam koleksi asing, hasil tanpa kecocokan akan dihapus.
Sebagai catatan kecil, ini sebenarnya adalah pola yang dioptimalkan seperti yang dijelaskan dalam $lookup + $unwind Coalescence
dalam dokumentasi inti. Hal khusus terjadi di sini di mana $unwind
instruksi sebenarnya digabungkan ke dalam $lookup
operasi dengan cara yang efisien. Anda dapat membaca lebih lanjut tentang itu di sana.
Menggunakan populate
Dari konten di atas, pada dasarnya Anda harus dapat memahami mengapa populate()
di sini adalah hal yang salah untuk dilakukan. Selain fakta dasar bahwa output tidak lagi terdiri dari Warranty
objek model, model itu benar-benar hanya tahu tentang item asing yang dijelaskan di _accountId
properti yang tidak ada di output.
Sekarang Anda bisa sebenarnya mendefinisikan model yang dapat digunakan untuk secara eksplisit melemparkan objek output ke dalam tipe output yang ditentukan. Demonstrasi singkat akan melibatkan penambahan kode ke aplikasi Anda untuk ini seperti:
// 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
baru ini model kemudian dapat digunakan untuk "melempar" objek JavaScript biasa yang dihasilkan ke dalam Dokumen Mongoose sehingga metode seperti Model.populate()
sebenarnya bisa dipanggil:
// 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);
Sejak Output
memiliki skema yang ditentukan yang mengetahui "referensi" pada _id
bidang dokumennya Model.populate()
mengetahui apa yang perlu dilakukan dan mengembalikan item.
Namun berhati-hatilah karena ini sebenarnya menghasilkan kueri lain. yaitu:
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: {} })
Di mana baris pertama adalah output agregat, dan kemudian Anda menghubungi server lagi untuk mengembalikan Account
terkait entri model.
Ringkasan
Jadi itu adalah pilihan Anda, tetapi harus cukup jelas bahwa pendekatan modern untuk ini adalah menggunakan $lookup
dan dapatkan "gabung" yang sebenarnya yang bukan populate()
sebenarnya sedang dilakukan.
Termasuk adalah daftar sebagai demonstrasi penuh tentang bagaimana masing-masing pendekatan ini benar-benar bekerja dalam praktik. Beberapa lisensi artistik diambil di sini, jadi model yang diwakili mungkin tidak persis sama dengan yang Anda miliki, tetapi cukup untuk mendemonstrasikan konsep dasar dengan cara yang dapat direproduksi:
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()
}
})()
Dan hasil lengkapnya:
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"
}
]
}
]