Популяция
В MongoDB оператор агрегации $lookup доступен начиная с версии >= 3.2. В Mongoose есть более мощная альтернатива, называемая populate(), которая позволяет ссылаться на документы в других коллекциях.
Популяция — это процесс автоматической замены указанных путей в документе на документы из других коллекций. Мы можем выполнить популяцию одного документа, нескольких документов, простого объекта, нескольких простых объектов или всех объектов, возвращаемых запросом. Давайте рассмотрим несколько примеров.
var mongoose = require('mongoose'); var Schema = mongoose.Schema; var personSchema = Schema({ _id: Schema.Types.ObjectId, name: String, age: Number, stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }] }); var storySchema = Schema({ author: { type: Schema.Types.ObjectId, ref: 'Person' }, title: String, fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }] }); var Story = mongoose.model('Story', storySchema); var Person = mongoose.model('Person', personSchema);
До сих пор мы создали два модели. Наша Person модель имеет поле stories , установленное в массив ObjectId. Опция ref сообщает Mongoose, какую модель использовать во время популяции, в нашем случае модель Story. Все _id , которые мы сохраняем здесь, должны быть документами _id из модели Story.
Примечание: ObjectId, Number, String, и Buffer допустимы для использования в качестве ссылок. Однако, вы должны использовать ObjectId, если вы не являетесь опытным пользователем и у вас нет веской причины для этого.
Сохранение ссылок
Сохранение ссылок на другие документы выполняется так же, как и обычное сохранение свойств, просто присвойте значение _id:
var author = new Person({ _id: new mongoose.Types.ObjectId(), name: 'Ian Fleming', age: 50 }); author.save(function (err) { if (err) return handleError(err); var story1 = new Story({ title: 'Casino Royale', author: author._id // assign the _id from the person }); story1.save(function (err) { if (err) return handleError(err); // thats it! }); });
Популяция
До сих пор мы ничего особо не меняли. Мы просто создали Person и Story. Теперь давайте посмотрим на популяцию author нашей истории, используя конструктор запросов:
Story.
findOne({ title: 'Casino Royale' }).
populate('author').
exec(function (err, story) {
if (err) return handleError(err);
console.log('The author is %s', story.author.name);
// prints "The author is Ian Fleming"
});
Популяционные пути больше не содержат своих исходных _id значений; их значение заменяется документом mongoose, возвращенным из базы данных путем выполнения отдельного запроса перед возвратом результатов.
Массивы ссылок работают аналогичным образом. Просто вызовите метод populate в запросе, и вместо исходных _id будет возвращен массив документов на месте.
Примечание: mongoose >= 3.6 предоставляет исходные _ids , используемые при популяции, через метод document#populated().
Установка полей популяции
В Mongoose >= 4.0 вы можете вручную выполнить популяцию поля.
Story.findOne({ title: 'Casino Royale' }, function(error, story) {
if (error) {
return handleError(error);
}
story.author = author;
console.log(story.author.name); // prints "Ian Fleming"
});
Обратите внимание, что это работает только для одиночных ссылок. В настоящее время вы не можете вручную выполнить популяцию массива ссылок.
Выбор полей
Что если мы хотим получить только несколько определенных полей для документов, полученных из популяции? Это можно сделать, передав обычный синтаксис имен полей в качестве второго аргумента методу populate:
Story.
findOne({ title: /casino royale/i }).
populate('author', 'name'). // only return the Persons name
exec(function (err, story) {
if (err) return handleError(err);
console.log('The author is %s', story.author.name);
// prints "The author is Ian Fleming"
console.log('The authors age is %s', story.author.age);
// prints "The authors age is null'
})
Популяция нескольких путей
Что если мы хотим популировать несколько путей одновременно?
Story. find(...). populate('fans'). populate('author'). exec()
Если вы вызываете populate() несколько раз с одним и тем же путем, будет действовать только последняя команда.
// The 2nd `populate()` call below overwrites the first because they // both populate 'fans'. Story. find(). populate({ path: 'fans', select: 'name' }). populate({ path: 'fans', select: 'email' }); // The above is equivalent to: Story.find().populate({ path: 'fans', select: 'email' });
Условия запроса и другие параметры
Что если мы хотели получить наш массив fans, отфильтрованный по возрасту, выбрать только имена и вернуть не более 5 из них?
Story.
find(...).
populate({
path: 'fans',
match: { age: { $gte: 21 }},
// Explicitly exclude `_id`, see http://bit.ly/2aEfTdB
select: 'name -_id',
options: { limit: 5 }
}).
exec()
Ссылки на дочерние элементы
Однако, если мы используем объект author, мы не можем получить список историй. Это происходит потому, что ни один объект story не был добавлен в author.stories.
Здесь есть две точки зрения. Во-первых, вы можете захотеть, чтобы author знал, какие истории принадлежат им. Обычно ваша схема должна решать отношения один-ко-многим, имея указатель на родительский элемент в стороне «многие». Но если у вас есть веские основания для использования массива указателей на дочерние элементы, вы можете добавить push() документы в массив, как показано ниже.
author.stories.push(story1); author.save(callback);
Это позволяет нам использовать сочетание find и populate:
Person.
findOne({ name: 'Ian Fleming' }).
populate('stories'). // only works if we pushed refs to children
exec(function (err, person) {
if (err) return handleError(err);
console.log(person);
});
Спорно, действительно ли мы хотим два набора указателей, поскольку они могут стать несинхронизированными. Вместо этого мы могли бы пропустить популяцию и напрямую find() истории, которые нас интересуют.
Story.
find({ author: author._id }).
exec(function (err, stories) {
if (err) return handleError(err);
console.log('The stories are an array: ', stories);
});
Документы, возвращенные из популяции запроса, становятся полностью функциональными, remove и save докумен-тами, если не указан параметр lean. Не путайте их с поддокументами. Будьте осторожны при вызове метода remove, так как вы удалите документ из базы данных, а не только из массива.
Популяция существующего документа
Если у нас есть существующий документ mongoose и мы хотим популировать некоторые из его путей, mongoose >= 3.6 поддерживает метод document#populate().
Популяция нескольких существующих документов
Если у нас есть один или несколько документов mongoose или даже простые объекты (например, вывод mapReduce), мы можем выполнить их популяцию с помощью метода Model.populate(), доступного в mongoose >= 3.6. Именно так document#populate() и query#populate() выполняют популяцию документов.
Популяция на нескольких уровнях
Предположим, у вас есть схема пользователя, которая отслеживает друзей пользователя.
var userSchema = new Schema({ name: String, friends: [{ type: ObjectId, ref: 'User' }] });
Популяция позволяет получить список друзей пользователя, но что если вы также хотели бы получить список друзей друзей пользователя? Укажите опцию populate , чтобы сообщить mongoose о популяции массива friends всех друзей пользователя:
User.
findOne({ name: 'Val' }).
populate({
path: 'friends',
// Get friends of friends - populate the 'friends' array for every friend
populate: { path: 'friends' }
});
Популяция между базами данных
Предположим, у вас есть схема, представляющая события, и схема, представляющая разговоры. Каждое событие имеет соответствующую ветвь разговора.
var eventSchema = new Schema({ name: String, // The id of the corresponding conversation // Notice there's no ref here! conversation: ObjectId }); var conversationSchema = new Schema({ numMessages: Number });
Кроме того, предположим, что события и разговоры хранятся в отдельных экземплярах MongoDB.
var db1 = mongoose.createConnection('localhost:27000/db1'); var db2 = mongoose.createConnection('localhost:27001/db2'); var Event = db1.model('Event', eventSchema); var Conversation = db2.model('Conversation', conversationSchema);
В этой ситуации вы не сможете выполнить популяцию обычным способом. Поле populate() всегда будет null, потому что conversation не знает, какую модель использовать. Однако, вы можете явно указать модель.
Event.
find().
populate({ path: 'conversation', model: Conversation }).
exec(function(error, docs) { /* ... */ });
Это известно как «популяция между базами данных», так как она позволяет вам популировать данные между базами данных MongoDB и даже между экземплярами MongoDB.
Динамические ссылки
Mongoose также может популировать данные из нескольких коллекций одновременно. Предположим, у вас есть схема пользователя, которая имеет массив «connections» (соединений) — пользователь может быть подключен либо к другим пользователям, либо к организации.
var userSchema = new Schema({ name: String, connections: [{ kind: String, item: { type: ObjectId, refPath: 'connections.kind' } }] }); var organizationSchema = new Schema({ name: String, kind: String }); var User = mongoose.model('User', userSchema); var Organization = mongoose.model('Organization', organizationSchema);
Свойство refPath выше означает, что mongoose будет анализировать путь connections.kind , чтобы определить, какую модель использовать для populate(). Другими словами, свойство refPath делает свойство ref динамическим.
// Say we have one organization: // `{ _id: ObjectId('000000000000000000000001'), name: "Guns N' Roses", kind: 'Band' }` // And two users: // { // _id: ObjectId('000000000000000000000002') // name: 'Axl Rose', // connections: [ // { kind: 'User', item: ObjectId('000000000000000000000003') }, // { kind: 'Organization', item: ObjectId('000000000000000000000001') } // ] // }, // { // _id: ObjectId('000000000000000000000003') // name: 'Slash', // connections: [] // } User. findOne({ name: 'Axl Rose' }). populate('connections.item'). exec(function(error, doc) { // doc.connections[0].item is a User doc // doc.connections[1].item is an Organization doc });
Популяция виртуальных свойств
Новое в 4.5.0
До сих пор вы выполняли популяцию только на основе поля _id. Однако иногда это не правильный выбор. В частности, массивы, которые растут без ограничений, являются антипаттерном MongoDB. Используя виртуальные свойства mongoose, вы можете определить более сложные отношения между документами.
var PersonSchema = new Schema({ name: String, band: String }); var BandSchema = new Schema({ name: String }); BandSchema.virtual('members', { ref: 'Person', // The model to use localField: 'name', // Find people where `localField` foreignField: 'band', // is equal to `foreignField` // If `justOne` is true, 'members' will be a single doc as opposed to // an array. `justOne` is false by default. justOne: false }); var Person = mongoose.model('Person', PersonSchema); var Band = mongoose.model('Band', BandSchema); /** * Suppose you have 2 bands: "Guns N' Roses" and "Motley Crue" * And 4 people: "Axl Rose" and "Slash" with "Guns N' Roses", and * "Vince Neil" and "Nikki Sixx" with "Motley Crue" */ Band.find({}).populate('members').exec(function(error, bands) { /* `bands.members` is now an array of instances of `Person` */ });
Помните, что виртуальные свойства не включаются в вывод toJSON() по умолчанию. Если вы хотите, чтобы виртуальные свойства отображались при использовании функций, которые полагаются на JSON.stringify(), например, функция Express res.json(), установите опцию virtuals: true в опциях схемы toJSON.
// Set `virtuals: true` so `res.json()` works var BandSchema = new Schema({ name: String }, { toJSON: { virtuals: true } });
Если вы используете проекцию populate, убедитесь, что foreignField включена в проекцию.
Band.
find({}).
populate({ path: 'members', select: 'name' }).
exec(function(error, bands) {
// Won't work, foreign field `band` is not selected in the projection
});
Band.
find({}).
populate({ path: 'members', select: 'name band' }).
exec(function(error, bands) {
// Works, foreign field `band` is selected
});
Далее
Теперь, когда мы рассмотрели популяцию запросов, давайте посмотрим на соединения.
© 2010 LearnBoost
Licensed under the MIT License.
https://mongoosejs.com/docs/4.x/docs/populate.html