Заполнение
MongoDB имеет оператор агрегации, похожий на объединение $lookup, в версиях >= 3.2. Mongoose имеет более мощную альтернативу, называемую populate(), которая позволяет ссылаться на документы в других коллекциях.
Заполнение — это процесс автоматической замены указанных путей в документе на документ(ы) из других коллекций. Мы можем заполнить один документ, несколько документов, простой объект, несколько простых объектов или все объекты, возвращаемые запросом. Давайте рассмотрим некоторые примеры.
const mongoose = require('mongoose');
const { Schema } = mongoose;
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
});
const storySchema = Schema({
author: { type: Schema.Types.ObjectId, ref: 'Person' },
title: String,
fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]
});
const Story = mongoose.model('Story', storySchema);
const Person = mongoose.model('Person', personSchema);
До сих пор мы создали две модели. Наша Person модель имеет свое поле stories, установленное в массив ObjectId. Опция ref — то, что сообщает Mongoose, какую модель использовать во время заполнения, в нашем случае модель Story. Все _id , которые мы храним здесь, должны быть документами _id из модели Story.
Сохранение ссылок
Сохранение ссылок на другие документы работает так же, как обычно сохраняются свойства, просто присвойте значение _id:
const author = new Person({
_id: new mongoose.Types.ObjectId(),
name: 'Ian Fleming',
age: 50
});
await author.save();
const story1 = new Story({
title: 'Casino Royale',
author: author._id // assign the _id from the person
});
await story1.save();
// that's it!
Вы можете установить опцию ref для путей ObjectId, Number, String и Buffer. populate() работает с ObjectIds, числами, строками и буферами. Однако мы рекомендуем использовать ObjectIds в качестве _id свойств (и, следовательно, ObjectIds для ref свойств), если у вас нет веской причины для отказа от этого. Это связано с тем, что MongoDB установит _id в ObjectId, если вы создадите новый документ без свойства _id, поэтому, если вы сделаете свой _id свойством типа Number, вам нужно быть очень внимательными, чтобы не вставить документ без числового _id.
Заполнение
До сих пор мы не делали ничего сильно отличающегося. Мы просто создали Person и Story. Теперь давайте посмотрим на заполнение author нашей истории, используя конструктор запросов:
const story = await Story.
findOne({ title: 'Casino Royale' }).
populate('author').
exec();
// prints "The author is Ian Fleming"
console.log('The author is %s', story.author.name);
Заполненные пути больше не установлены в их первоначальные _id, их значение заменяется документом Mongoose, возвращаемым из базы данных, путем выполнения отдельного запроса перед возвратом результатов.
Массивы ссылок работают аналогичным образом. Просто вызовите метод populate в запросе, и массив документов будет возвращен вместо исходных _id.
Установка заполненных полей
Вы можете вручную заполнить свойство, установив его в документ. Документ должен быть экземпляром модели, к которой относится ваше свойство ref.
const story = await Story.findOne({ title: 'Casino Royale' });
story.author = author;
console.log(story.author.name); // prints "Ian Fleming"
Проверка заполнения поля
Вы можете вызвать функцию populated(), чтобы проверить, заполнено ли поле. Если populated() возвращает истинное значение, вы можете предположить, что поле заполнено.
story.populated('author'); // truthy
story.depopulate('author'); // Make `author` not populated anymore
story.populated('author'); // undefined
Частая причина для проверки заполненности пути — получение идентификатора author. Однако для вашего удобства Mongoose добавляет _id геттер к экземплярам ObjectId, поэтому вы можете использовать story.author._id независимо от того, заполнено ли author.
story.populated('author'); // truthy
story.author._id; // ObjectId
story.depopulate('author'); // Make `author` not populated anymore
story.populated('author'); // undefined
story.author instanceof ObjectId; // true
story.author._id; // ObjectId, because Mongoose adds a special getter
Что делать, если нет внешнего документа?
Mongoose populate не ведет себя как обычные SQL-объединения. Когда нет документа, story.author будет null. Это аналогично левому объединению в SQL.
await Person.deleteMany({ name: 'Ian Fleming' });
const story = await Story.findOne({ title: 'Casino Royale' }).populate('author');
story.author; // `null`
Если у вас есть массив authors в вашей storySchema, populate() вернет пустой массив.
const storySchema = Schema({
authors: [{ type: Schema.Types.ObjectId, ref: 'Person' }],
title: String
});
// Later
const story = await Story.findOne({ title: 'Casino Royale' }).populate('authors');
story.authors; // `[]`
Выбор поля
А что, если мы хотим получить только несколько определенных полей для заполненных документов? Это можно сделать, передав обычный синтаксис имени поля в качестве второго аргумента методу populate:
const story = await Story.
findOne({ title: /casino royale/i }).
populate('author', 'name').
exec(); // only return the Persons name
// prints "The author is Ian Fleming"
console.log('The author is %s', story.author.name);
// prints "The authors age is null"
console.log('The authors age is %s', story.author.age);
Заполнение нескольких путей
А что, если нам нужно заполнить сразу несколько путей?
await Story.
find({ /* ... */ }).
populate('fans').
populate('author').
exec();
Если вы вызываете populate() несколько раз с одним и тем же путем, в силу вступит только последний.
// The 2nd `populate()` call below overwrites the first because they
// both populate 'fans'.
await Story.
find().
populate({ path: 'fans', select: 'name' }).
populate({ path: 'fans', select: 'email' });
// The above is equivalent to:
await Story.find().populate({ path: 'fans', select: 'email' });
Условия запроса и другие параметры
Что, если мы хотим заполнить массив наших поклонников (fans) на основе их возраста и выбрать только их имена?
await Story.
find().
populate({
path: 'fans',
match: { age: { $gte: 21 } },
// Explicitly exclude `_id`, see http://bit.ly/2aEfTdB
select: 'name -_id'
}).
exec();
Опция match не отфильтровывает Story документы. Если нет документов, удовлетворяющих match, вы получите Story документ с пустым массивом fans.
Например, предположим, что вы populate() поле author истории, и author не удовлетворяет match. Тогда author будет null.
const story = await Story.
findOne({ title: 'Casino Royale' }).
populate({ path: 'author', name: { $ne: 'Ian Fleming' } }).
exec();
story.author; // `null`
В общем случае нет способа заставить populate() фильтровать истории на основе свойств author истории. Например, запрос ниже не вернет результатов, даже если author заполнено.
const story = await Story.
findOne({ 'author.name': 'Ian Fleming' }).
populate('author').
exec();
story; // null
Если вы хотите фильтровать истории по имени автора, следует использовать денормализацию.
limit против perDocumentLimit
Заполнение поддерживает опцию limit, однако в настоящее время для обеспечения обратной совместимости не ограничено на уровне документа. Например, предположим, что у вас есть 2 истории:
await Story.create([
{ title: 'Casino Royale', fans: [1, 2, 3, 4, 5, 6, 7, 8] },
{ title: 'Live and Let Die', fans: [9, 10] }
]);
Если вы попытаетесь populate() с опцией limit, вы обнаружите, что у второй истории 0 поклонников:
const stories = await Story.find().populate({
path: 'fans',
options: { limit: 2 }
});
stories[0].name; // 'Casino Royale'
stories[0].fans.length; // 2
// 2nd story has 0 fans!
stories[1].name; // 'Live and Let Die'
stories[1].fans.length; // 0
Это происходит потому, что для избежания выполнения отдельного запроса для каждого документа, Mongoose запрашивает поклонников (fans) используя numDocuments * limit в качестве предела. Если вам нужен правильный limit, вы должны использовать опцию perDocumentLimit (новая в Mongoose 5.9.0). Просто имейте в виду, что populate() выполнит отдельный запрос для каждой истории, что может привести к тому, что populate() станет медленнее.
const stories = await Story.find().populate({
path: 'fans',
// Special option that tells Mongoose to execute a separate query
// for each `story` to make sure we get 2 fans for each story.
perDocumentLimit: 2
});
stories[0].name; // 'Casino Royale'
stories[0].fans.length; // 2
stories[1].name; // 'Live and Let Die'
stories[1].fans.length; // 2
Ссылки на дочерние элементы
Однако, если мы используем объект author, мы не сможем получить список историй. Это связано с тем, что никакие объекты story никогда не были добавлены в author.stories.
Здесь есть две точки зрения. Во-первых, вы можете захотеть, чтобы author знал, какие истории принадлежат им. Обычно ваша схема должна разрешать отношения «один ко многим», имея указатель родителя на стороне «многих». Но если у вас есть веская причина для того, чтобы иметь массив указателей на дочерние элементы, вы можете добавлять документы push() в массив, как показано ниже.
await story1.save();
author.stories.push(story1);
await author.save();
Это позволяет нам выполнить комбинацию find и populate:
const person = await Person.
findOne({ name: 'Ian Fleming' }).
populate('stories').
exec(); // only works if we pushed refs to children
console.log(person);
Спорно, действительно ли нам нужны два набора указателей, так как они могут стать несинхронизированными. Вместо этого мы можем пропустить заполнение и напрямую find() истории, которые нас интересуют.
const stories = await Story.
find({ author: author._id }).
exec();
console.log('The stories are an array: ', stories);
Документы, возвращаемые при заполнении запроса запроса, становятся полностью функциональными, remove-и, save-и документами, если не указана опция lean. Не путайте их с поддокументами. Будьте осторожны при вызове метода remove, так как вы будете удалять его из базы данных, а не только из массива.
Заполнение существующего документа
Если у вас есть существующий документ mongoose и вы хотите заполнить некоторые его пути, вы можете использовать метод Document#populate().
const person = await Person.findOne({ name: 'Ian Fleming' });
person.populated('stories'); // null
// Call the `populate()` method on a document to populate a path.
await person.populate('stories');
person.populated('stories'); // Array of ObjectIds
person.stories[0].name; // 'Casino Royale'
Метод Document#populate() не поддерживает цепочки. Вам нужно вызывать populate() несколько раз или с массивом путей, чтобы заполнить несколько путей.
await person.populate(['stories', 'fans']);
person.populated('fans'); // Array of ObjectIds
Заполнение нескольких существующих документов
Если у нас есть один или несколько документов mongoose или даже простые объекты (например, выход mapReduce), мы можем заполнить их, используя метод Model.populate(). Именно это используют Document#populate() и Query#populate() для заполнения документов.
Заполнение на нескольких уровнях
Представьте, что у вас есть схема пользователя, которая отслеживает друзей пользователя.
const userSchema = new Schema({
name: String,
friends: [{ type: ObjectId, ref: 'User' }]
});
Заполнение позволяет получить список друзей пользователя, но что, если вы также хотели список друзей друзей пользователя? Укажите опцию populate , чтобы сообщить Mongoose о заполнении массива friends всех друзей пользователя:
await User.
findOne({ name: 'Val' }).
populate({
path: 'friends',
// Get friends of friends - populate the 'friends' array for every friend
populate: { path: 'friends' }
});
Заполнение между базами данных
Допустим, у вас есть схема, представляющая события, и схема, представляющая разговоры. Каждое событие имеет соответствующую ветвь разговора.
const db1 = mongoose.createConnection('mongodb://127.0.0.1:27000/db1');
const db2 = mongoose.createConnection('mongodb://127.0.0.1:27001/db2');
const conversationSchema = new Schema({ numMessages: Number });
const Conversation = db2.model('Conversation', conversationSchema);
const eventSchema = new Schema({
name: String,
conversation: {
type: ObjectId,
ref: Conversation // `ref` is a **Model class**, not a string
}
});
const Event = db1.model('Event', eventSchema);
В приведенном выше примере события и разговоры хранятся в отдельных базах данных MongoDB. Строка ref не сработает в этом случае, так как Mongoose предполагает, что строка ref относится к имени модели на том же соединении. В приведенном выше примере модель разговора зарегистрирована на db2, а не на db1.
// Works
const events = await Event.
find().
populate('conversation');
Это известно как "заполнение между базами данных", поскольку позволяет вам заполнять между базами данных MongoDB и даже между экземплярами MongoDB.
Если у вас нет доступа к экземпляру модели при определении вашего eventSchema, вы также можете передать экземпляр модели в качестве параметра populate().
const events = await Event.
find().
// The `model` option specifies the model to use for populating.
populate({ path: 'conversation', model: Conversation });
Динамические ссылки через refPath
Mongoose также может заполнять из нескольких коллекций на основе значения свойства в документе. Допустим, вы разрабатываете схему для хранения комментариев. Пользователь может оставлять комментарии к записи в блоге или продукту.
const commentSchema = new Schema({
body: { type: String, required: true },
doc: {
type: Schema.Types.ObjectId,
required: true,
// Instead of a hardcoded model name in `ref`, `refPath` means Mongoose
// will look at the `docModel` property to find the right model.
refPath: 'docModel'
},
docModel: {
type: String,
required: true,
enum: ['BlogPost', 'Product']
}
});
const Product = mongoose.model('Product', new Schema({ name: String }));
const BlogPost = mongoose.model('BlogPost', new Schema({ title: String }));
const Comment = mongoose.model('Comment', commentSchema);
Опция refPath — более сложная альтернатива ref. Если ref — строка, Mongoose всегда будет запрашивать ту же модель для поиска заполненных поддокументов. С помощью refPath, вы можете настроить, какую модель использовать Mongoose для каждого документа.
const book = await Product.create({ name: 'The Count of Monte Cristo' });
const post = await BlogPost.create({ title: 'Top 10 French Novels' });
const commentOnBook = await Comment.create({
body: 'Great read',
doc: book._id,
docModel: 'Product'
});
const commentOnPost = await Comment.create({
body: 'Very informative',
doc: post._id,
docModel: 'BlogPost'
});
// The below `populate()` works even though one comment references the
// 'Product' collection and the other references the 'BlogPost' collection.
const comments = await Comment.find().populate('doc').sort({ body: 1 });
comments[0].doc.name; // "The Count of Monte Cristo"
comments[1].doc.title; // "Top 10 French Novels"
Альтернативный подход заключается в определении отдельных blogPost и product свойств в commentSchema, а затем populate() для обоих свойств.
const commentSchema = new Schema({
body: { type: String, required: true },
product: {
type: Schema.Types.ObjectId,
required: true,
ref: 'Product'
},
blogPost: {
type: Schema.Types.ObjectId,
required: true,
ref: 'BlogPost'
}
});
// ...
// The below `populate()` is equivalent to the `refPath` approach, you
// just need to make sure you `populate()` both `product` and `blogPost`.
const comments = await Comment.find().
populate('product').
populate('blogPost').
sort({ body: 1 });
comments[0].product.name; // "The Count of Monte Cristo"
comments[1].blogPost.title; // "Top 10 French Novels"
Определение отдельных свойств blogPost и product работает для этого простого примера. Но если вы решите разрешить пользователям также комментировать статьи или другие комментарии, вам нужно будет добавить больше свойств в вашу схему. Вам также понадобится дополнительный вызов populate() для каждого свойства, если вы не используете mongoose-autopopulate. Использование refPath означает, что вам нужно всего 2 пути схемы и один вызов populate() независимо от того, к скольким моделям может указывать ваш commentSchema.
Заполнение виртуальных свойств
До сих пор вы заполняли только по полю _id. Однако иногда это не лучший выбор. Например, предположим, что у вас есть две модели: Author и BlogPost.
const AuthorSchema = new Schema({
name: String,
posts: [{ type: mongoose.Schema.Types.ObjectId, ref: 'BlogPost' }]
});
const BlogPostSchema = new Schema({
title: String,
comments: [{
author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' },
content: String
}]
});
const Author = mongoose.model('Author', AuthorSchema, 'Author');
const BlogPost = mongoose.model('BlogPost', BlogPostSchema, 'BlogPost');
Выше приведён пример плохого проектирования схемы. Почему? Предположим, у вас есть чрезвычайно плодовитый автор, который пишет более 10 000 статей блога. Этот документ author будет огромным, более 12 Кб, а большие документы приводят к проблемам производительности как на сервере, так и на клиенте. Принцип наименьшей кратности гласит, что отношения «один ко многим», как, например, автор и статья блога, должны храниться на стороне «многих». Другими словами, статьи блога должны хранить своего author, авторы не должны хранить все свои posts.
const AuthorSchema = new Schema({
name: String
});
const BlogPostSchema = new Schema({
title: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' },
comments: [{
author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' },
content: String
}]
});
К сожалению, эти две схемы в написанном виде не поддерживают заполнение списка статей блога автора. Здесь на помощь приходит виртуальное заполнение. Виртуальное заполнение означает вызов populate() для виртуального свойства, которое имеет опцию ref, как показано ниже.
// Specifying a virtual with a `ref` property is how you enable virtual
// population
AuthorSchema.virtual('posts', {
ref: 'BlogPost',
localField: '_id',
foreignField: 'author'
});
const Author = mongoose.model('Author', AuthorSchema, 'Author');
const BlogPost = mongoose.model('BlogPost', BlogPostSchema, 'BlogPost');
Затем вы можете заполнить список статей блога автора, как показано ниже.
const author = await Author.findOne().populate('posts');
author.posts[0].title; // Title of the first blog post
Обратите внимание, что виртуальные свойства не включаются в toJSON() и toObject() вывод по умолчанию. Если вы хотите, чтобы виртуальные свойства заполнения отображались при использовании функций, таких как функция Express' res.json() или console.log(), установите опцию virtuals: true в опциях схемы toJSON и toObject().
const authorSchema = new Schema({ name: String }, {
toJSON: { virtuals: true }, // So `res.json()` and other `JSON.stringify()` functions include virtuals
toObject: { virtuals: true } // So `console.log()` and other functions that use `toObject()` include virtuals
});
Если вы используете проекции заполнения, убедитесь, что foreignField включена в проекцию.
let authors = await Author.
find({}).
// Won't work because the foreign field `author` is not selected
populate({ path: 'posts', select: 'title' }).
exec();
authors = await Author.
find({}).
// Works, foreign field `author` is selected
populate({ path: 'posts', select: 'title author' }).
exec();
Заполнение виртуальных свойств: опция подсчёта
Виртуальные свойства заполнения также поддерживают подсчёт количества документов, соответствующих foreignField, а не самих документов. Установите опцию count для вашего виртуального свойства:
const PersonSchema = new Schema({
name: String,
band: String
});
const BandSchema = new Schema({
name: String
});
BandSchema.virtual('numMembers', {
ref: 'Person', // The model to use
localField: 'name', // Find people where `localField`
foreignField: 'band', // is equal to `foreignField`
count: true // And only get the number of docs
});
// Later
const doc = await Band.findOne({ name: 'Motley Crue' }).
populate('numMembers');
doc.numMembers; // 2
Заполнение виртуальных свойств: опция соответствия
Ещё одной опцией для виртуальных свойств заполнения является match. Эта опция добавляет дополнительное условие фильтрации к запросу, который Mongoose использует для populate():
// Same example as 'Populate Virtuals' section
AuthorSchema.virtual('posts', {
ref: 'BlogPost',
localField: '_id',
foreignField: 'author',
match: { archived: false } // match option with basic query selector
});
const Author = mongoose.model('Author', AuthorSchema, 'Author');
const BlogPost = mongoose.model('BlogPost', BlogPostSchema, 'BlogPost');
// After population
const author = await Author.findOne().populate('posts');
author.posts; // Array of not `archived` posts
Вы также можете установить опцию match в функцию. Это позволяет настраивать match в зависимости от заполняемого документа. Например, предположим, что вам нужны только статьи блога, чьи tags содержат один из favoriteTags автора.
AuthorSchema.virtual('posts', {
ref: 'BlogPost',
localField: '_id',
foreignField: 'author',
// Add an additional filter `{ tags: author.favoriteTags }` to the populate query
// Mongoose calls the `match` function with the document being populated as the
// first argument.
match: author => ({ tags: author.favoriteTags })
});
Вы можете перезаписать опцию match при вызове populate() следующим образом.
// Overwrite the `match` option specified in `AuthorSchema.virtual()` for this
// single `populate()` call.
await Author.findOne().populate({ path: posts, match: {} });
Вы также можете установить опцию match в функцию в вашем вызове populate() . Если вы хотите объединить вашу опцию соответствия populate(), а не перезаписывать её, используйте следующее.
await Author.findOne().populate({
path: posts,
// Add `isDeleted: false` to the virtual's default `match`, so the `match`
// option would be `{ tags: author.favoriteTags, isDeleted: false }`
match: (author, virtual) => ({
...virtual.options.match(author),
isDeleted: false
})
});
Заполнение карт
Карты — это тип, представляющий объект с произвольными строковыми ключами. Например, в схеме ниже members представляет собой карту из строк в ObjectId.
const BandSchema = new Schema({
name: String,
members: {
type: Map,
of: {
type: 'ObjectId',
ref: 'Person'
}
}
});
const Band = mongoose.model('Band', bandSchema);
Эта карта имеет ref, что означает, что вы можете использовать populate() для заполнения всех ObjectId в карте. Предположим, у вас есть документ band ниже:
const person1 = new Person({ name: 'Vince Neil' });
const person2 = new Person({ name: 'Mick Mars' });
const band = new Band({
name: 'Motley Crue',
members: {
singer: person1._id,
guitarist: person2._id
}
});
Вы можете заполнить каждый элемент в карте, заполнив специальный путь members.$*. $* — специальная синтаксическая конструкция, которая сообщает Mongoose просматривать каждый ключ в карте.
const band = await Band.findOne({ name: 'Motley Crue' }).populate('members.$*');
band.members.get('singer'); // { _id: ..., name: 'Vince Neil' }
Вы также можете заполнять пути в картах поддокументов с помощью $* . Например, предположим, что у вас есть librarySchema ниже:
const librarySchema = new Schema({
name: String,
books: {
type: Map,
of: new Schema({
title: String,
author: {
type: 'ObjectId',
ref: 'Person'
}
})
}
});
const Library = mongoose.model('Library', librarySchema);
Вы можете заполнить автора каждой книги, заполнив books.$*.author:
const libraries = await Library.find().populate('books.$*.author');
Заполнение в обработчиках
Вы можете заполнять данные в предварительных или последующих обработчиках. Если вы хотите всегда заполнять определённое поле, ознакомьтесь с плагином mongoose-autopopulate.
// Always attach `populate()` to `find()` calls
MySchema.pre('find', function() {
this.populate('user');
});
// Always `populate()` after `find()` calls. Useful if you want to selectively populate
// based on the docs found.
MySchema.post('find', async function(docs) {
for (const doc of docs) {
if (doc.isPublic) {
await doc.populate('user');
}
}
});
// `populate()` after saving. Useful for sending populated data back to the client in an
// update API endpoint
MySchema.post('save', function(doc, next) {
doc.populate('user').then(function() {
next();
});
});
Заполнение нескольких путей в обработчиках
Заполнение нескольких путей в обработчиках может быть полезно, когда вы всегда хотите заполнять некоторые поля. Но реализация немного сложнее, чем вы можете себе представить. Вот как вы, возможно, ожидаете, что это будет работать:
const userSchema = new Schema({
email: String,
password: String,
followers: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
following: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]
});
userSchema.pre('find', function(next) {
this.populate('followers following');
next();
});
const User = mongoose.model('User', userSchema);
Однако это не сработает. По умолчанию передача нескольких путей в populate() в обработчике вызовет бесконечную рекурсию, что означает, что он по существу вызовет тот же обработчик для всех путей, предоставленных методу populate() - Например, this.populate('followers following') вызовет тот же обработчик для полей followers и following, и запрос просто зависнет в бесконечном цикле.
Чтобы этого избежать, необходимо добавить опцию _recursed, чтобы наш обработчик не заполнял данные рекурсивно. Приведенный ниже пример позволит ему работать как ожидается.
userSchema.pre('find', function(next) {
if (this.options._recursed) {
return next();
}
this.populate({ path: 'followers following', options: { _recursed: true } });
next();
});
В качестве альтернативы, вы можете ознакомиться с плагином mongoose-autopopulate.
Преобразование заполненных документов
Вы можете манипулировать заполненными документами с помощью опции transform . Если вы укажете функцию transform, Mongoose вызовет эту функцию для каждого заполненного документа в результате с двумя аргументами: заполненный документ и исходный идентификатор, используемый для заполнения документа. Это даёт вам больший контроль над результатом выполнения populate() . Это особенно полезно при заполнении нескольких документов.
Исходная мотивация опции transform заключалась в возможности оставить незаполненное _id , если документ не был найден, вместо того, чтобы устанавливать значение в null:
// With `transform`
doc = await Parent.findById(doc).populate([
{
path: 'child',
// If `doc` is null, use the original id instead
transform: (doc, id) => doc == null ? id : doc
}
]);
doc.child; // 634d1a5744efe65ae09142f9
doc.children; // [ 634d1a67ac15090a0ca6c0ea, { _id: 634d1a4ddb804d17d95d1c7f, name: 'Luke', __v: 0 } ]
Вы можете вернуть любое значение из transform() . Например, вы можете использовать transform() для «сглаживания» заполненных документов следующим образом.
let doc = await Parent.create({ children: [{ name: 'Luke' }, { name: 'Leia' }] });
doc = await Parent.findById(doc).populate([{
path: 'children',
transform: doc => doc == null ? null : doc.name
}]);
doc.children; // ['Luke', 'Leia']
Ещё одним случаем использования transform() является установка значений $locals на заполненных документах для передачи параметров в геттеры и виртуальные свойства. Например, предположим, что вы хотите установить код языка в своём документе для целей интернационализации, как показано ниже.
const internationalizedStringSchema = new Schema({
en: String,
es: String
});
const ingredientSchema = new Schema({
// Instead of setting `name` to just a string, set `name` to a map
// of language codes to strings.
name: {
type: internationalizedStringSchema,
// When you access `name`, pull the document's locale
get: function(value) {
return value[this.$locals.language || 'en'];
}
}
});
const recipeSchema = new Schema({
ingredients: [{ type: mongoose.ObjectId, ref: 'Ingredient' }]
});
const Ingredient = mongoose.model('Ingredient', ingredientSchema);
const Recipe = mongoose.model('Recipe', recipeSchema);
Вы можете установить код языка для всех заполненных упражнений следующим образом:
// Create some sample data
const { _id } = await Ingredient.create({
name: {
en: 'Eggs',
es: 'Huevos'
}
});
await Recipe.create({ ingredients: [_id] });
// Populate with setting `$locals.language` for internationalization
const language = 'es';
const recipes = await Recipe.find().populate({
path: 'ingredients',
transform: function(doc) {
doc.$locals.language = language;
return doc;
}
});
// Gets the ingredient's name in Spanish `name.es`
recipes[0].ingredients[0].name; // 'Huevos'
© 2010 LearnBoost
Licensed under the MIT License.
https://mongoosejs.com/docs/populate.html