Was Sie hier im Grunde übersehen haben, ist der "Pfad" zu dem Feld, das Sie populate()
möchten ist eigentlich 'portfolio.formatType'
und nicht nur 'portfolio'
wie du geschrieben hast. Aufgrund dieses Fehlers und der Struktur könnten Sie jedoch einige allgemeine Missverständnisse haben.
Korrektur auffüllen
Die grundlegende Korrektur benötigt lediglich den korrekten Pfad, und Sie benötigen nicht das model
Argument, da dies bereits im Schema enthalten ist:
User.findById(req.params.id).populate('portfolio.formatType');
Es ist jedoch im Allgemeinen keine gute Idee, sowohl „eingebettete“ Daten als auch „referenzierte“ Daten innerhalb von Arrays zu „mischen“, und Sie sollten wirklich entweder alles einbetten oder einfach auf alles verweisen. Es ist im Allgemeinen auch ein bisschen ein "Anti-Muster", eine Reihe von Referenzen im Dokument beizubehalten, wenn Sie referenzieren möchten, da Ihr Grund darin bestehen sollte, das Dokument nicht über die 16-MB-BSON-Grenze hinaus wachsen zu lassen. Und wo diese Grenze von Ihren Daten niemals erreicht würde, ist es im Allgemeinen besser, "vollständig einzubetten". Das ist wirklich eine breitere Diskussion, aber etwas, dessen Sie sich bewusst sein sollten.
Der nächste allgemeine Punkt hier ist populate()
selbst ist etwas "alter Hut" und wirklich nicht das "magische", was die meisten neuen Benutzer es wahrnehmen. Um klar zu sein populate()
ist KEIN JOIN , und alles, was es tut, ist eine weitere Abfrage an den Server auszuführen, um die "verwandten" Elemente zurückzugeben, und dann diesen Inhalt mit den Dokumenten zusammenzuführen, die von der vorherigen Abfrage zurückgegeben wurden.
$lookup-Alternative
Wenn Sie nach "Joins" suchen, dann wollten Sie wahrscheinlich, wie bereits erwähnt, "Einbetten". Dies ist wirklich der "MongoDB-Weg", um mit "Beziehungen" umzugehen, aber alle "zusammengehörigen" Daten in einem Dokument zusammenzuhalten. Die andere Möglichkeit einer „Verknüpfung“, bei der sich Daten in separaten Sammlungen befinden, erfolgt über $lookup
Operator in modernen Releases.
Dies wird aufgrund Ihrer "gemischten" Inhalts-Array-Form etwas komplexer, kann aber im Allgemeinen wie folgt dargestellt werden:
// 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" ] }
]
}
}
}
}
}}
]);
Oder mit der aussagekräftigeren Form von $lookup
seit 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",
}}
]
}}
]);
Die beiden Ansätze funktionieren etwas unterschiedlich, aber beide arbeiten im Wesentlichen mit dem Konzept, die übereinstimmenden "verwandten" Einträge zurückzugeben und dann auf den vorhandenen Array-Inhalt "neu zuzuordnen", um ihn mit dem "name"
zu verschmelzen Eigenschaften, die in das Array "eingebettet" sind. Das ist eigentlich die Hauptkomplikation, die ansonsten eine ziemlich einfache Methode zum Abrufen ist.
Es ist so ziemlich derselbe Vorgang wie bei populate()
tatsächlich auf dem "Client", sondern auf dem "Server" ausgeführt. Die Vergleiche verwenden also $indexOfArray
Operator, um zu finden, wo die passende ObjectId
ist Werte sind und dann eine Eigenschaft aus dem Array an diesem übereinstimmenden "Index" über $arrayElemAt
Betrieb.
Der einzige Unterschied besteht darin, dass wir in der mit MongoDB 3.6 kompatiblen Version diese „Ersetzung“ innerhalb des „fremden“ Inhalts „before“ vornehmen Die verbundenen Ergebnisse werden an das übergeordnete Element zurückgegeben. In früheren Versionen geben wir das gesamte übereinstimmende fremde Array zurück und "verheiraten" die beiden dann, um ein einzelnes "zusammengeführtes" Array zu bilden, indem wir $map
.
Auch wenn diese zunächst „komplexer“ aussehen mögen, besteht der große Vorteil hier darin, dass es sich um eine „einzelne Anfrage“ handelt an den Server mit einer "Single Response" und nicht "mehrere" Anfragen als populate()
ausgeben und empfangen tut. Dies spart tatsächlich eine Menge Overhead im Netzwerkverkehr und erhöht die Antwortzeit erheblich.
Darüber hinaus sind dies "echte Verknüpfungen", sodass Sie viel mehr tun können, was mit "mehreren Abfragen" nicht erreicht werden kann. Beispielsweise können Sie die Ergebnisse beim „Join“ „sortieren“ und nur die besten Ergebnisse zurückgeben, indem Sie populate()
verwenden muss "alle Eltern" einziehen, bevor es überhaupt nach den "Kindern" suchen kann, die als Ergebnis zurückgegeben werden sollen. Dasselbe gilt auch für das "Filtern" von Bedingungen für den untergeordneten "Beitritt".
Weitere Einzelheiten dazu finden Sie unter Querying after populate in Mongoose über die allgemeinen Einschränkungen und was Sie tatsächlich tun können, um die Generierung solcher "komplexer" Aggregation-Pipeline-Anweisungen bei Bedarf zu "automatisieren".
Demonstration
Ein weiteres häufiges Problem bei diesen "Joins" und dem Verständnis von referenzierten Schemas im Allgemeinen besteht darin, dass die Leute oft falsch verstehen, wo und wann die Referenzen gespeichert werden sollen und wie das alles funktioniert. Daher dienen die folgenden Auflistungen als Demonstration sowohl der Speicherung als auch des Abrufs solcher Daten.
In einer nativen Promises-Implementierung für ältere NodeJS-Versionen:
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());
})()
Und mit async/await
Syntax für neuere NodeJS-Releases, einschließlich der aktuellen LTS v.8.x-Reihe:
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()
}
})()
Die letztgenannte Auflistung wird auf jeder Stufe kommentiert, um die Teile zu erklären, und Sie können zumindest im Vergleich sehen, wie sich beide Formen der Syntax zueinander verhalten.
Beachten Sie, dass der „ausdrucksstarke“ $lookup
Das Beispiel wird nur ausgeführt, wenn der verbundene MongoDB-Server die Syntax tatsächlich unterstützt.
Und die "Ausgabe" für diejenigen, die sich nicht die Mühe machen, den Code selbst auszuführen:
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
}
]
}