Apa yang pada dasarnya Anda lewatkan di sini adalah "jalur" ke bidang yang ingin Anda populate()
sebenarnya 'portfolio.formatType'
dan bukan hanya 'portfolio'
seperti yang telah Anda ketik. Karena kesalahan dan struktur tersebut, Anda mungkin memiliki beberapa kesalahpahaman umum.
Mengisi Koreksi
Koreksi dasar hanya membutuhkan jalur yang benar, dan Anda tidak memerlukan model
argumen karena ini sudah tersirat dalam skema:
User.findById(req.params.id).populate('portfolio.formatType');
Namun umumnya bukan ide yang bagus untuk "mencampur" data "tertanam" dan data "direferensikan" dalam array, dan Anda harus benar-benar menyematkan semuanya atau hanya mereferensikan semuanya. Ini juga sedikit "anti-pola" secara umum untuk menyimpan berbagai referensi dalam dokumen jika niat Anda adalah referensi, karena alasan Anda seharusnya tidak menyebabkan dokumen tumbuh melampaui batas BSON 16MB. Dan di mana batas itu tidak akan pernah tercapai oleh data Anda, umumnya lebih baik untuk "menyematkan sepenuhnya". Itu benar-benar diskusi yang lebih luas, tetapi sesuatu yang harus Anda ketahui.
Poin umum berikutnya di sini adalah populate()
itu sendiri agak "topi lama", dan benar-benar bukan hal "ajaib" yang dianggap sebagian besar pengguna baru. Agar jelas populate()
adalah BUKAN GABUNG , dan yang dilakukannya hanyalah mengeksekusi kueri lain ke server untuk mengembalikan item "terkait", lalu menggabungkan konten tersebut ke dalam dokumen yang dikembalikan dari kueri sebelumnya.
$lookup Alternatif
Jika Anda mencari "gabungan", maka Anda mungkin benar-benar ingin "menyematkan" seperti yang disebutkan sebelumnya. Ini benar-benar "Cara MongoDB" dalam menangani "hubungan" tetapi menyimpan semua data "terkait" bersama dalam satu dokumen. Cara lain untuk "bergabung" di mana data berada dalam koleksi terpisah adalah melalui $lookup
operator dalam rilis modern.
Ini menjadi sedikit lebih rumit karena bentuk larik konten "campuran" Anda, tetapi secara umum dapat direpresentasikan sebagai:
// 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" ] }
]
}
}
}
}
}}
]);
Atau dengan bentuk yang lebih ekspresif dari $lookup
sejak 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",
}}
]
}}
]);
Kedua pendekatan bekerja sedikit berbeda, tetapi keduanya pada dasarnya bekerja dengan konsep mengembalikan entri "terkait" yang cocok dan kemudian "memetakan ulang" ke konten array yang ada untuk digabungkan dengan "name"
properti "tertanam" di dalam array. Itu sebenarnya komplikasi utama yang sebaliknya adalah metode pengambilan yang cukup mudah.
Prosesnya hampir sama dengan apa yang populate()
sebenarnya tidak pada "klien" tetapi dieksekusi pada "server". Jadi perbandingannya menggunakan $indexOfArray
operator untuk menemukan di mana pencocokan ObjectId
nilai adalah dan kemudian mengembalikan properti dari larik pada "indeks" yang cocok melalui $arrayElemAt
operasi.
Satu-satunya perbedaan adalah bahwa dalam versi yang kompatibel dengan MongoDB 3.6, kami melakukan "substitusi" dalam konten "asing" "sebelum" hasil bergabung dikembalikan ke induk. Dalam rilis sebelumnya, kami mengembalikan seluruh larik asing yang cocok dan kemudian "menikahkan" keduanya untuk membentuk larik "gabungan" tunggal menggunakan $map
.
Meskipun awalnya mungkin terlihat "lebih rumit", keuntungan besar di sini adalah bahwa ini merupakan "permintaan tunggal" ke server dengan "respon tunggal" dan tidak mengeluarkan dan menerima permintaan "beberapa" sebagai populate()
melakukan. Ini sebenarnya menghemat banyak overhead dalam lalu lintas jaringan dan sangat meningkatkan waktu respons.
Selain itu, ini adalah "gabungan nyata" sehingga ada banyak lagi yang dapat Anda lakukan yang tidak dapat dicapai dengan "beberapa kueri". Misalnya Anda dapat "mengurutkan" hasil pada "bergabung" dan hanya mengembalikan hasil teratas, sedangkan menggunakan populate()
perlu menarik "semua orang tua" bahkan sebelum dapat mencari "anak-anak" mana yang akan dikembalikan sebagai hasilnya. Hal yang sama berlaku untuk kondisi "pemfilteran" pada anak "bergabung" juga.
Ada beberapa detail lebih lanjut tentang ini di Meminta kueri setelah diisi di Mongoose tentang batasan umum dan apa yang sebenarnya dapat Anda lakukan secara praktis untuk "mengotomatiskan" pembuatan pernyataan pipa agregasi "kompleks" jika diperlukan.
Demonstrasi
Masalah umum lainnya dengan melakukan "gabungan" ini dan memahami skema yang direferensikan secara umum adalah bahwa orang sering salah memahami konsep di mana dan kapan harus menyimpan referensi dan bagaimana semuanya bekerja. Oleh karena itu, daftar berikut berfungsi sebagai demonstrasi penyimpanan dan pengambilan data tersebut.
Dalam implementasi Promises asli untuk rilis NodeJS yang lebih lama:
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());
})()
Dan dengan async/await
sintaks untuk rilis NodeJS yang lebih baru, termasuk seri LTS v.8.x saat ini:
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()
}
})()
Daftar terakhir jika dikomentari pada setiap tahap untuk menjelaskan bagian-bagiannya, dan Anda setidaknya dapat melihat dengan perbandingan bagaimana kedua bentuk sintaks tersebut saling berhubungan.
Perhatikan bahwa "ekspresif" $lookup
contoh hanya berjalan di mana server MongoDB yang terhubung benar-benar mendukung sintaks.
Dan "keluaran" bagi mereka yang tidak dapat diganggu untuk menjalankan kode sendiri:
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
}
]
}