Zasadniczo przegapiłeś tutaj „ścieżkę” do pola, które chcesz populate()
to w rzeczywistości 'portfolio.formatType'
a nie tylko 'portfolio'
jak wpisałeś. Z powodu tego błędu i struktury możesz mieć jednak kilka ogólnych nieporozumień.
Wypełnij korektę
Podstawowa korekta wymaga jedynie prawidłowej ścieżki, a ty nie potrzebujesz model
argument, ponieważ jest to już zawarte w schemacie:
User.findById(req.params.id).populate('portfolio.formatType');
Jednak generalnie nie jest dobrym pomysłem "mieszanie" zarówno "osadzone" danych, jak i "odnośnych" danych w tablicach, i tak naprawdę powinieneś albo osadzać wszystko, albo po prostu odwoływać się do wszystkiego. Jest to również trochę "anty-wzorcem" w ogóle, aby zachować tablicę odniesień w dokumencie, jeśli masz zamiar się odwoływać, ponieważ Twoim powodem nie powinno być powodowanie, że dokument przekroczy limit 16 MB BSON. A tam, gdzie Twoje dane nigdy nie osiągną tego limitu, generalnie lepiej jest „w pełni osadzić”. To naprawdę szersza dyskusja, ale coś, o czym powinieneś wiedzieć.
Następnym ogólnym punktem tutaj jest populate()
samo w sobie jest trochę „starym kapeluszem” i tak naprawdę nie jest „magiczną” rzeczą, za którą większość nowych użytkowników ją postrzega. Aby wyczyścić populate()
to NIE DOŁĄCZENIE , a wszystko, co robi, to wykonanie kolejnego zapytania do serwera w celu zwrócenia „powiązanych” elementów, a następnie scalenie tej treści z dokumentami zwróconymi z poprzedniego zapytania.
$lookup alternatywa
Jeśli szukasz „połączeń”, to prawdopodobnie chciałeś „osadzenia”, jak wspomniano wcześniej. To jest naprawdę "Sposób MongoDB" radzenia sobie z "relacjami", ale utrzymywanie wszystkich "powiązanych" danych w jednym dokumencie. Innym sposobem „dołączenia”, w którym dane znajdują się w oddzielnych zbiorach, jest użycie $lookup
operator w nowoczesnych wydaniach.
Staje się to nieco bardziej skomplikowane ze względu na twoją „mieszaną” formę tablicy treści, ale generalnie może być reprezentowane jako:
// 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" ] }
]
}
}
}
}
}}
]);
Lub w bardziej wyrazistej formie $lookup
od 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",
}}
]
}}
]);
Te dwa podejścia działają nieco inaczej, ale oba zasadniczo działają z koncepcją zwrócenia pasujących „powiązanych” wpisów, a następnie „ponownego mapowania” na istniejącą zawartość tablicy w celu scalenia z "name"
właściwości „osadzone” wewnątrz tablicy. To jest właściwie główna komplikacja, która w przeciwnym razie jest dość prostą metodą odzyskiwania.
To prawie ten sam proces, co populate()
faktycznie działa na „kliencie”, ale jest wykonywany na „serwerze”. Zatem porównania używają $indexOfArray
operator, aby znaleźć pasujący ObjectId
wartości są, a następnie zwracają właściwość z tablicy o dopasowanym „indeksie” za pośrednictwem $arrayElemAt
operacja.
Jedyna różnica polega na tym, że w wersji zgodnej z MongoDB 3.6 wykonujemy to „podstawienie” w „obcej” treści „przed” połączone wyniki są zwracane do rodzica. W poprzednich wersjach zwracamy całą pasującą obcą tablicę, a następnie „połączamy” obie, aby utworzyć pojedynczą „połączoną” tablicę za pomocą $map
.
Chociaż początkowo mogą one wyglądać na „bardziej złożone”, dużą zaletą jest to, że stanowią one „pojedyncze żądanie” do serwera z „pojedynczą odpowiedzią” i nie wystawiać i nie otrzymywać „wielu” żądań jako populate()
czy. W rzeczywistości oszczędza to dużo ruchu sieciowego i znacznie wydłuża czas odpowiedzi.
Ponadto są to „prawdziwe sprzężenia”, więc można zrobić o wiele więcej, czego nie można osiągnąć za pomocą „wielu zapytań”. Na przykład możesz "sortować" wyniki po "dołączeniu" i zwracać tylko najlepsze wyniki, gdzie jak przy użyciu populate()
musi przyciągnąć „wszystkich rodziców”, zanim będzie mógł nawet szukać, które „dzieci” w rezultacie wrócą. To samo dotyczy warunków „filtrowania” w przypadku „dołączenia” dziecka.
Więcej szczegółów na ten temat znajdziesz w Pytaniu po wypełnieniu w Mongoose o ogólnych ograniczeniach i o tym, co faktycznie możesz zrobić, aby „zautomatyzować” generowanie takich „złożonych” instrukcji potoku agregacji tam, gdzie jest to potrzebne.
Demonstracja
Innym częstym problemem związanym z wykonywaniem tych „połączeń” i ogólnym zrozumieniem schematu odniesienia jest to, że ludzie często mylą się z pojęciami, gdzie i kiedy przechowywać odniesienia i jak to wszystko działa. Dlatego poniższe wykazy służą jako demonstracja zarówno przechowywania, jak i wyszukiwania takich danych.
W natywnej implementacji Promises dla starszych wersji NodeJS:
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());
})()
Oraz z async/await
składnia nowszych wydań NodeJS, w tym bieżącej serii LTS v.8.x:
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()
}
})()
Ta ostatnia lista jest skomentowana na każdym etapie w celu wyjaśnienia części, a przez porównanie możesz przynajmniej zobaczyć, jak obie formy składni odnoszą się do siebie.
Zwróć uwagę, że „ekspresyjny” $lookup
przykład działa tylko wtedy, gdy podłączony serwer MongoDB faktycznie obsługuje składnię.
I „wyjście” dla tych, którym nie przeszkadza samo uruchomienie kodu:
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
}
]
}