- Быстрый старт
- Схема
- SchemaTypes
- Подключение
- Модель
- Документ
- Субдокумент
- Запрос
- Валидация
- Промежуточный слой
- Популяция
- API
Mongoose
предоставляет простое основанное на схемах решение для моделирования данных приложения. Он включает встроенную проверку типов, валидацию, формирование запросов и хуки, отвечающие за реализацию дополнительной логики обработки запросов.
const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost:27017/test', {
useNewUrlParser: true,
useUnifiedTopology: true
})
const Cat = mongoose.model('Cat', { name: String })
const kitty = new Cat({ name: 'Cat' })
kitty.save().then(() => console.log('мяу'))
Установка
yarn add mongoose
# или
npm i mongoose
Подключение к БД
const mongoose = require('mongoose')
const { MONGODB_URI } = require('./config.js')
mongoose.connect(MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
Обработка ошибки и подключения
const db = mongoose.connection
db.on('error', console.error.bind(console, 'Ошибка подключения: '))
db.once('open', () => {
// Подлючение установлено
})
Создание схемы и модели
const { Schema, model } = mongoose
const catSchema = new Schema({
name: String
})
module.exports = model('Cat', catSchema)
Создание объекта
const Cat = require('./Cat.js')
const kitty = new Cat({ name: 'Cat' })
Добавление метода
catSchema.methods.speak = function () {
const greet = this.name
? `Меня зовут ${this.name}`
: `У меня нет имени`
console.log(greet)
}
const kitty = new Cat({ name: 'Cat' })
kitty.speak() // Меня зовут Cat
Сохранение объекта
kitty.save((err, cat) => {
if (err) console.error(err)
cat.speak()
})
Поиск всех объектов определенной модели
Cat.find((err, cats) => {
if (err) console.error(err)
console.log(cats)
})
Поиск одного объекта (по имени)
Cat.findOne({ name: 'Cat' }, callback)
Для поиска объекта можно использовать регулярное выражение.
Создание схемы
const { Schema } = required('mongoose')
const blogSchema = new Schema({
title: String,
author: String
body: String
comments: [
{
body: String
createdAt: Date
}
],
createdAt: {
type: Date,
default: Date.now()
},
hidden: Boolean,
meta: {
votes: Number,
favs: Number
}
})
Дополнительные поля можно добавлять с помощью метода Schema.add()
const firstSchema = new Schema()
firstSchema.add({
name: 'string',
color: 'string',
price: 'number'
})
const secondSchema = new Schema()
secondSchema.add(firstSchema).add({ year: Number })
По умолчанию Mongoose
добавляет к схеме свойство _id
(его можно перезаписывать)
const schema = new Schema()
schema.path('_id') // ObjectId { ... }
const Model = model('Test', schema)
const doc = new Model()
doc._id // ObjectId { ... }
Кроме методов экземпляра, схема позволяет определять статические функции
catSchema.statics.findByName = function(name) {
return this.find({ name: new RegExp(name, 'i') })
}
// или
catSchema.static('findByName', function(name) { return this.find({ name: new RegExp(name, 'i') }) })
const Cat = model('Cat', catSchema)
const cats = await Cat.findByName('Kitty')
Также для определения дополнительного функционала можно использовать утилиты для формирования запроса
catSchema.query.byName = function(name) { return this.where({ name: new RegExp(name, 'i') }) }
const Cat = model('Cat', catSchema)
Cat.find().byName('Cat').exec((err, cats) => {
console.log(cats)
})
Cat.findOne().byName('Cat').exec((err, cat) => {
console.log(cat)
})
Mongoose
позволяет определять вторичные индексы в Schema
на уровне пути или на уровне schema
const catSchema = new Schema({
name: String,
type: String,
tags: { type: [String], index: true } // уровень поля
})
catSchema.index({ name: 1, type: -1 }) // уровень схемы
При запуске приложения для каждого индекса вызывается createIndex()
. Индексы полезны для разработки, но в продакшне их лучше не использовать. Отключить индексацию можно с помощью autoIndex: false
mongoose.connect(MONGODB_URL, { autoIndex: false })
// или
catSchema.set('autoIndex', false)
// или
new Schema({...}, { autoIndex: false })
Схема позволяет создавать виртуальные свойства, которые можно получать и устанавливать, но которые не записываются в БД
// Схема
const personSchema = new Schema({
name: {
first: String,
last: String
}
})
// Модель
const Person = model('Person', personSchema)
// Документ
const john = new Person({
name: {
first: 'John',
last: 'Smith'
}
})
// Виртуальное свойство для получения и записи полного имени
personSchema.virtual('fullName')
.get(function() { return `${this.name.first} ${this.name.last}` })
.set(function(str) {
;[this.name.first, this.name.last] = str.split(' ')
})
john.fullName // John Smith
john.fullName = 'Jane Air'
john.fullName // Jane Air
Настройки схемы могут передаваться в виде объекта в конструктор или в виде пары ключ/значение в метод set()
new Schema({...}, options)
const schema = new Schema({...})
schema.set(option, value)
autoIndex: bool
- определяет создание индексов (по умолчаниюtrue
)autoCreate: bool
- определяет создание коллекций (по умолчаниюfalse
)bufferCommands: bool
иbufferTimaoutMs: num
- определяет, должны ли команды буферизоваться (по умолчаниюtrue
), и в течение какого времени (по умолчанию отсутствует)capped: num
- позволяет создавать закрытые коллекции ({ capped: 1024 }
, 1024 - размер в байтах)collection: str
- определяет название коллекции (по умолчанию используется название модели)id: bool
- позволяет отключать получение_id
через виртуальный геттерid
_id: bool
- позволяет отключать создание_id
minimize: bool
- определяет удаление пустых объектов (по умолчаниюtrue
). Для определения пустого объекта используется утилита$isEmpty
-doc.$isEmpty(fieldName)
strict: bool
- определяет, должны ли значения, передаваемые в конструктор модели и отсутствующие в схеме, сохраняться в БД (по умолчаниюtrue
, значит, не должны)typeKey: str
- позволяет определять ключ типа (по умолчаниюtype
)validateBeforeSave: bool
- позволяет отключать валидацию объектов перед их сохранением в БД (по умолчаниюtrue
)collation: obj
- определяет порядок разрешения коллизий, например, при совпадении двух объектов ({collation: { locale: 'en_US', strength: 1 }}
- совпадающие ключи/значения на латинице будут игнорироваться)timestamps: bool | obj
- позволяет добавлять к схеме поляcreatedAt
иupdatedAt
с типомDate
. Данным полям можно присваивать другие названия -{ timestamps: { createdAt: 'created_at' } }
. По умолчанию для создания даты используетсяnew Date()
. Это можно изменить -{ timestamps: { currentTime: () => ~~(Date.now() / 1000) } }
Метод loadClass()
позволяет создавать схемы из классов:
- методы класса становятся методами
Mongoose
- статические методы класса становятся статическими методами
Mongoose
- геттеры и сеттеры становятся виртуальными методами
Mongoose
class MyClass {
myMethod() { return 42 }
static myStatic() { return 24 }
get myVirtual() { return 31 }
}
const schema = new Schema()
schema.loadClass(MyClass)
console.log(schema.methods) // { myMethod: ... }
console.log(schema.statics) // { myStatic: ... }
console.log(schema.virtuals) // { myVirtual: ... }
Схема представляет собой объект конфигурации для модели. SchemaType
- это объект конфигурации для определенного свойства модели. SchemaType
определяет тип, геттеры, сеттеры и валидаторы свойства.
Доступные SchemaTypes
(типы)
- String
- Number
- Date
- Buffer
- Boolean
- Mixed
- ObjectId
- Array
- Decimal128
- Map
const schema = new Schema({
name: String,
binary: Buffer,
livilng: Boolean,
updated: {
type: Date,
default: Date.now
},
age: {
type: Number,
min: 18,
max: 65
},
mixed: Schema.Types.Mixed,
_someId: Schema.Types.ObjectId,
decimal: Schema.Types.Decimal128,
array: [],
ofString: [String],
ofNumber: [Number],
ofDates: [Date],
ofBuffer: [Buffer],
ofBoolean: [Boolean],
ofMixed: [Schema.Types.Mixed],
ofObjectId: [Schema.Types.ObjectId],
ofArrays: [[]],
ofArrayOfNumber: [[Number]],
nested: {
stuff: {
type: String,
lowercase: true,
trim: true
}
},
map: Map,
mapOfString: {
type: Map,
of: String
}
})
Существует три способа определения типа
// 1
name: String
// 2
name: 'String' | 'string'
// 3
name: {
type: String
}
Для определения свойства с названием type
необходимо сделать следующее
new Schema({
asset: {
// !
type: { type: String },
ticker: String
}
})
При определении типа схемы с помощью объекта можно определять дополнительные настройки.
required: bool
- определяет, является ли поле обязательнымdefault: any | fn
- значение поля по умолчаниюvalidate: fn
- функция-валидаторget: fn
- кастомный геттерset: fn
- кастомный сеттерalias: str
- алиас, синоним, виртуальный геттер/сеттерimmutable: bool
- определяет, является ли поле иммутабельным (неизменяемым)transform: fn
- функция, вызываемая при выполненииDocument.toJSON()
, включаяJSON.stringify()
const numberSchema = new Schema({
intOnly: {
type: Number,
get: v => Math.round(v),
set: v => Math.round(v),
alias: 'i'
}
})
const Number = model('Number', numberSchema)
const doc = new Number()
doc.intOnly = 2.001
doc.intOnly // 2
doc.i // 2
doc.i = 3.002
doc.intOnly // 3
doc.i // 3
index: bool
unique: bool
- определяет, должны ли индексы быть уникальнымиsparse: bool
- определяет, должны ли индексы создаваться только для индексируемых полей
lowercase: bool
uppercase: bool
trim: bool
match: RegExp
- валидатор в виде регулярного выраженияenum: arr
- валидатор, проверяющий, совпадает ли строка с каким-либо элементом массиваminLength: num
maxLength: num
populate: obj
- дефолтные настройки популяции (извлечения данных, заполнения данными)
min: num
max: num
enum: arr
populate: obj
min: Date
max: Date
populate: obj
При изменении даты, например, с помощью метода setMonth()
, ее необходимо пометить с помощью markModified()
, иначе, изменения не будут сохранены
const schema = new Schema('date', { dueDate: Date })
schema.findOne((err, doc) => {
doc.dueDate.setMonth(3)
// это не сработает
doc.save()
// надо делать так
doc.markModified('dueDate')
doc.save()
})
Mongoose
не осуществляет проверку смешанных типов
new Schema({ any: {} })
new Schema({ any: Object })
new Schema({ any: Schema.Types.Mixed })
new Schema({ any: mongoose.Mixed })
При изменении значений смешанного типа, их, как и даты, необходимо помечать с помощью markModified
.
ObjectId
- это специальный тип, обычно используемый для уникальных идентификаторов
const carSchema = new Schema({ driver: mongoose.ObjectId })
ObjectId
- это класс, а сами ObjectIds
- это объекты, которые, обычно, представлены в виде строки. При преобразовании ObjectId
с помощью метода toString()
, мы получаем 24-значную шестнадцатиричную строку
const Car = model('Car', carSchema)
const car = new Car()
car.driver.toString() // 5e1a0651741b255ddda996c4
MongooseMap
- это подкласс JS-класса Map
. Ключи карт должны быть строками
const userSchema = new Schema({
socialMediaHandles: {
type: Map,
of: String
}
})
const User = model('User', userSchema)
const user = new User({
socialMediaHandles: {}
})
Для получения и установки значений следует использовать get()
и set()
user.socialMediaHandles.set('github', 'username')
// или
user.set('socialMediaHandles.github', 'username')
user.socialMediaHandles.get('github') // username
// или
user.get('socialMediaHandles.github') // username
Для популяции элементов в карту используется синтаксис $*
const userSchema = new Schema({
socialMediaHandles: {
type: Map,
of: new Schema({
handle: String,
oauth: {
type: ObjectId,
ref: 'OAuth'
}
})
}
})
const User = model('User', userSchema)
// заполняем свойство `oauth` данными пользователя
const user = await User.findOne().populate('socialMediaHandles.$*.oauth')
Геттеры похожи на виртуальные свойства для полей схемы. Предположим, что мы хотим сохранять аватар пользователя в виде относительного пути и затем добавлять к нему название хоста в приложении
const root = 'https://examplce.com/avatars'
const userSchema = new Schema({
name: String,
avatar: {
type: String,
get: v => `${root}/${v}`
}
})
const User = model('User', userSchema)
const user = new User({ name: 'John', avatar: 'john.png' })
user.avatar // https://examplce.com/avatars/john.png
В качестве типов полей схемы можно использовать другие схемы
const subSchema = new Schema({})
const schema = new Schema({
data: {
type: subSchema,
default: {}
}
})
Для подключения к MongoDB
используется метод mongoose.connect()
mongoose.connect(MONGODB_URL, { useNewUrlParser: true, useUnifiedTopology: true })
Данный метод принимает строку с адресом БД и объект с настройками.
За счет буферизации Mongoose
позволяет использовать модели, не дожидаясь подключения к БД. Отключить буферизацию можно с помощью bufferCommands: false
. Отключить ожидание создания коллекций можно с помощью autoCreate: false
const schema = new Schema({
name: String
}, {
capped: { size: 1024 },
bufferCommands: false,
autoCreate: false
})
Существует 2 класса ошибок, возникающих при подключении к БД:
- Ошибка при первоначальном подключении. При провале подключения
Mongoose
отправляет событиеerror
и промис, возвращаемыйconnect()
, отклоняется. При этом,Mongoose
не пытается выполнить повторное подключение - Ошибка после установки начального соединения. В этом случае
Mongoose
пытается выполнить повторное подключение
Для обработки первого класса ошибок используется catch()
или try/catch
mongoose.connect(uri, options)
.catch(err => handleError(err))
// или
try {
await mongoose.connect(uri, options)
} catch (err) {
handleError(err)
}
Для обработки второго класса ошибок нужно прослушивать соответствующее событие
mongoose.connection.on('error', err => {
handleError(err)
})
bufferCommands: bool
- позволяет отключать буферизациюuser: str
/pass: str
- имя пользователя и пароль для аутентификацииautoIndex: bool
- позволяет отключать автоматическую индексациюdbName: str
- название БД для подключения
Важные
useNewUrlParser: bool
-true
означает использование нового MongoDB-парсера для разбора строки подключения (должно быть включено во избежание предупреждений)useCreateIndex: bool
-true
означает использованиеcreateIndex()
вместоensureIndex()
для создания индексов (должно быть включено при индексации)useFindAndModify: bool
-false
означает использование нативногоfindOneAndUpdate()
вместоfindAndModify()
useUnifiedTopology: bool
-true
означает использование нового движка MongoDB для управления подключением (должно быть включено во избежание предупреждений)poolSize: num
- максимальное количество сокетов для данного подключенияsocketTimeoutMS: num
- время, по истечении которого неактивный сокет отключается от БДfamily: 4 | 6
- версия IP для подключенияauthSource: str
- БД, используемая при аутентификации с помощьюuser
иpass
Функция connect()
также принимает колбек, возвращающий промис
mongoose.connect(uri, options, (err) => {
if (err) // обработка ошибки
// успех
})
// или
mongoose.connect(uri, options).then(
() => { /* обработка ошибки */ },
err => { /* успех */ }
)
connecting
- начало подключенияconnected
- успешное подключениеopen
- эквивалентconnected
disconnecting
- приложение вызвалоConnection.close()
для отключения от БДdisconnected
- потеря подключенияclose
- возникает после выполненияConnection.close()
reconnected
- успешное повторное подключениеerror
- ошибка подключенияfullsetup
- возникает при подключении к набору реплик (replica set), когда выполнено подключение к основной реплике и одной из вторичныхall
- возникает при подключении к набору реплик, когда выполнено подключение ко всем серверамreconnectedFailed
- провал всехreconnectTries
(попыток выполнения повторного подключения)
mongoose.connect('mongodb://<username>:<password>@host1.com:27017,host2.com:27017,host3.com:27017/testdb')
// к одному узлу
mongoose.connect('mongodb://host1.com:port1/?replicaSet=rsName')
Подключение к нескольким БД выполняется с помощью метода createConnection()
. Существует два паттерна для выполнения такого подключения:
- Экспорт подключения и регистрация моделей для подключения в одном файле
// connections/fast.js
const mongoose = require('mongoose')
const conn = mongoose.createConnection(process.env.MONGO_URI)
conn.model('User', require('../schemas/user'))
module.exports = conn
// connections/slow.js
const mongoose = require('mongoose')
const conn = mongoose.createConnection(process.env.MONGO_URI)
conn.model('User', require('../schemas/user'))
conn.model('PageView', require('../schemas/pageView'))
module.exports = conn
- Регистрация подключений с функцией внедрения зависимостей
const mongoose = require('mongoose')
module.exports = function connFactory() {
const conn = mongoose.createConnection(process.env.MONGO_URI)
conn.model('User', require('../schemas/user'))
conn.model('PageView', require('../schemas/pageView'))
return conn
}
Модели - это конструкторы, компилируемые из определений Schema
. Экземпляр модели называется документом. Модели отвечают за создание и получение документов из БД.
При вызове mongoose.model()
происходит компиляция модели
const schema = new Schema({ name: 'string', age: 'number' })
const User = model('User', schema)
Первый аргумент - название коллекции. В данном случае будет создана коллекция users
.
Обратите внимание: функция model()
создает копию schema
. Убедитесь, что перед вызовом model()
полностью настроили schema
(включая определение хуков).
const User = model('User', userSchema)
const user = new User({ name: 'John', age: 30 })
user.save((err) => {
if (err) return handleError(err)
console.log(user)
})
// или
User.create({ name: 'John', age: 30 }, (err, user) => {
if (err) return handleError(err)
console.log(user)
})
// или для создания нескольких документов
User.insertMany([
{
name: 'John',
age: 30
},
{
name: 'Jane',
age: 20
}
], (err) => {})
Для получения документов из БД используются методы find()
, findById()
, findOne()
, where()
и др.
User.find({ name: 'John' }).where('createdAt').gt(oneYearAgo).exec(callback)
Для удаления документов используются методы deleteOne()
, deleteMany()
и др.
User.deleteOne({ name: 'John' }, (err) => {})
Для обновления документов используется метод updateOne()
и др.
User.updateOne({ name: 'John' }, { name: 'Bob' }, (err, res) => {
// Обновляется как минимум один документ
// `res.modifiedCount` содержит количество обновленных документов
// Обновленные документы не возвращаются
})
Если мы хотим обновить один документ и вернуть его, то следует использовать findOneAndUpdate()
.
Поток изменений позволяет регистрировать добавление/удаление и обновление документов
const run = async () => {
const userSchema = new Schema({
name: String
})
const User = model('User', userSchema)
// Создаем поток изменений
// При обновлении БД отправляется (emitted) событие `change`
User.watch()
.on('change', data => console.log(new Date(), data))
console.log(new Date(), 'Создание документа')
await User.create({ name: 'John Smith' })
console.log(new Date(), 'Документ успешно создан')
}
Документы - это экземпляры моделей. Документ и модель - разные классы Mongoose
. Класс модели является подклассом класса документа. При использовании конструктора модели создается новый документ.
const doc = await Model.findOne()
Документы можно изменять с помощью обычного присвоения - Mongoose
автоматически преобразует его в оператор обновления MongoDB
doc.name = 'foo'
// `Mongoose` отправляет `updateOne({ _id: doc._id }, { $set: { name: 'foo' } })` в `MongoDB`
await doc.save()
Метод save()
возвращает промис, который при успешном завершении операции возвращает сохраненный документ
doc.save().then((savedDoc) => {
savedDoc === doc // true
})
При отсутствии документа с указанным _id
, выбрасывается исключение DocumentNotFoundError
.
Если нам не хватает save()
, то можно создать собственное обновление
// Обновляем все документы в коллекции `users`
await User.updateMany({}, { $set: { name: 'John' } })
Обратите внимание, что update()
, updateMany()
, findOneAndUpdate()
и др. не запускают посредников save()
.
Документы подвергаются кастингу и валидации перед сохранением. Кастинг представляет собой проверку типа, а валидация - проверку дополнительных настроек (например, min
для типа Number
). Внутренне Mongoose
вызывает метод документа validate()
перед сохранением
const userSchema = new Schema({ name: String, age: { type: Number, min: 18 } })
const User = model('User', userSchema)
const user = new User({ name: 'John', age: 'many' })
// Кастинг типа `Number` провален для значения `many` поля `age`
await user.validate()
const user2 = new User(name: 'Jane', age: 17)
// Значение поля `age` (17) меньше минимально допустимого значения (18)
Существует два способа перезаписи документов (замены всех ключей документа). Первый способ - функции Document.overwrite()
и save()
const user = await User.findOne({ _id })
// Устанавливаем `name` и удаляем остальные свойства
user.overwrite({ name: 'John' })
await doc.save()
Второй способ - метод Model.replaceOne()
await User.replaceOne({ _id }, { name: 'John' })
Субдокументы - это документы, встроенные в другие документы. В Mongoose
это означает, что одни схемы могут быть вложены в другие.
const childSchema = new Schema({ name: String })
const parentSchema = new Schema({
// массив субдокументов
children: [childSchema],
// единичный вложенный субдокумент
child: childSchema
})
Субдокументы похожи на обычные документы. Вложенные схемы могут иметь посредников, кастомную логику валидации, виртуальные свойства и т.д. Главное отличие субдокументов состоит в том, что они не сохраняются индивидуально, они сохраняются вместе с родительским документом.
const Parent = model('Parent', parentSchema)
const parent = new Parent({ children: [
{ name: 'John' },
{ name:'Jane' }
] })
parent.children[0].name = 'Bob'
// `parent.children[0].save()` запустит посредников
// но не сохранит документ. Для этого нужно сохранить его предка
parent.save()
Вызов save()
предка запускает выполнение save()
всех потоков. Тоже самое справедливо для validate()
.
Дочерние посредники pre('save')
и pre('validate')
выполняются до родительского pre('save')
, но после родительского pre('validate')
// числа 1-4 будут выведены по порядку
const childSchema = new Schema({ name: String })
childSchema.pre('validate', (next) => {
console.log(2)
next()
})
childSchema.pre('save', (next) => {
console.log(3)
next()
})
const parentSchema = new Schema({
child: childSchema
})
parentSchema.pre('validate', (next) => {
console.log(1)
next()
})
parentSchema.pre('save', (next) => {
console.log(4)
next()
})
Вложенные пути немного отличаются от субдокументов. В приведенном ниже примере у нас имеется две схемы: одна с субдокументом child
, другая с вложенным путем child
// Субдокумент
const subdocSchema = new Schema({
child: new Schema({
name: String,
age: Number
})
})
const Subdoc = model('Subdoc', subdocSchema)
// Вложенный путь
const nestedSchema = new Schema({
child: {
name: String,
age: Number
}
})
const Nested = model('Nested', nestedSchema)
Отличия:
- экземпляры
Nested
никогда не имеютchild === undefined
. Мы может определять подсвойстваchild
даже при отсутствии самогоchild
. Но экземплярыSubdoc
могут иметьchild === undefined
const doc1 = new Subdoc({})
doc1.child === undefined // true
doc1.child.name = 'test' // TypeError: cannot read property...
const doc2 = new Nested({})
doc2.child === undefined // false
console.log(doc2.child) // MongooseDocument { undefined }
doc2.child.name = 'test' // работает
- метод
Document.set()
выполняет объединение для вложенных путей и перезапись для субдокументов
const doc1 = new Subdoc({
child: {
name: 'John',
age: 30
}
})
doc1.set({
child: {
age: 20
}
})
doc1.child // { age: 20 }
const doc2 = new Nested({
child: {
name: 'John',
age: 30
}
})
doc2.set({
child: {
age: 20
}
})
doc2.set({
child: {
age: 20
}
})
doc2.child // { name: 'John', age: 20 }
Пути субдокументов имеют значение undefined
(к ним не применяются настройки default
) до тех пор, пока им не будет присвоено ненулевое значение
const subdocSchema = new Schema({
child: new Schema({
name: String,
age: {
type: Number,
default: 18
}
})
})
const Subdoc = model('Subdoc', subdocSchema)
// настройка `default` поля `age` не будет иметь эффекта, поскольку значением `child` является `undefined`
const doc = new Subdoc()
doc.child // undefined
Для поиска субдокумента по его _id
(устанавливается по умолчанию) используется специальный метод id()
const subdoc = parent.children.id(_id)
Такие методы как push()
, unshift()
, addToSet()
и др. осуществляют прозрачный кастинг передаваемых аргументов
const Parent = model('Parent', parentSchema)
const parent = new Parent()
parent.children.push({ name: 'John' })
const subdoc = parent.children[0]
console.log(subdoc) // { _id: '...', name: 'John' }
subdoc.isNew // true
parent.save()
Для того, чтобы создать документ без его добавления в массив, следует использовать метод create()
const subdoc = parent.children.create({ name: 'Jane' })
У каждого субдокумента есть метод remove()
. Для массива субдокументов данный метод эквивалентен вызову pull()
. Для единичного субдокумента он эквивалентен установке значения субдокумента в null
// эквивалент `parent.children.pull(_id)`
parent.children.id(_id).remove()
// эквивалент `parent.child = null`
parent.child.remove()
parent.save()
Для получения непосредственного предка субдокумента используется функция parent()
, а для получения предка верхнего уровня (для глубоко вложенных субдокументов) - функция ownerDocument()
.
При создании схемы с помощью массива объектов, Mongoose
автоматически преобразует объекты в схемы
const parentSchema = new Schema({
children: [
{
name: String
}
]
})
// эквивалент
const parentSchema = new Schema({
children: [
new Schema({
name: String
})
]
})
Обратите внимание, что в случае единичного объекта, вместо схемы создается вложенных путь. Для того, чтобы сообщить Mongoose
о необходимости создания схемы для единичного объекта, следует использовать настройку typePojoToMixed: false
const schema = new Schema({
nested: {
// `new Schema({ prop: String })`
type: { prop: String },
required: true
}
}, { typePojoToMixed: false })
Модель предоставляет несколько статических вспомогательных функций. Каждая функция возвращает объект Query
Model.deleteMany()
Model.deleteOne()
Model.find()
Model.findById()
Model.findByIdAndDelete()
Model.findByIdAndRemove()
Model.findByIdAndUpdate()
Model.findOne()
Model.findOneAndDelete()
Model.findOneAndRemove()
Model.findOneAndReplace()
Model.findOneAndUpdate()
Model.replaceOne()
Model.updateMany()
Model.updateOne()
Запрос может выполняться двумя способами:
- с помощью колбека - запрос выполняется асинхронно и результат передается в колбек
- с помощью метода
then()
запроса, что позволяет использовать его как промис
При выполнении запроса с помощью callback
, запрос может быть определен в виде JSON документа
const User = model('User', userSchema)
// Ищем пользователей, фамилия которых совпадает с `Smith`, выбираем поля `name` и `occupation`
User.findOne(
{ 'name.last': 'Smith' },
'name occupation',
(err, user) => {
if (err) return handleError(err)
console.log(user)
}
)
Запрос выполняется, его результат передается в callback
. Сигнатура колбеков всегда такая: callback(error, result)
. При возникновении ошибки, параметр error
содержит объект ошибки, а result
является нулевым. При успешном выполнении запроса error
является нулевым, а result
заполняется (populate) данными из результата запроса.
result
зависит от операции: для findOne()
- это потенциально ненулевой единичный документ, для find()
- список документов, для count()
- количество документов, для update()
- количество обновленных документов и т.д.
Пример без колбека
// Ищем пользователей, фамилия которых совпадает с `Smith`
const query = User.findOne({ 'name.last', 'Smith' })
// выбираем поля `name` и `occupation`
query.select('name occupation')
// выполняем запрос
query.exec((err, user) => {
if (err) return handleError(err)
console.log(user)
})
В приведенном примере переменная query
имеет тип Query
. Query
позволяет формировать запрос двумя способами
// Документ JSON
User
.find({
'name.last': 'Smith',
// gt - greater than, больше чем; lt - less than, меньше чем
age: { $gt: 17, $lt: 66 },
// in - один из вариантов
likes: { $in: ['playing guitar', 'swimming'] }
})
.limit(10)
.sort({ occupation: -1 })
.select({ name: 1, occupation: 1 })
.exec(callback)
// Строитель (builder) запроса
User
.find({ 'name.last': 'Smith' })
.where('age').gt(17).lt(66)
.where('likes').in(['playing guitar', 'swimming'])
.limit(10)
.sort('-occupation')
.select('name occupation')
.exec(callback)
Несмотря на то, что у запросов есть метод then()
, это всего лишь соглашение, они не являются промисами. В отличие от промисов, вызов then()
запроса может привести к его многократному выполнению.
В приведенном ниже примере updateMany()
вызывается 3 раза
const q = Model.updateMany({}, {completed: true}, () => {
console.log('Первое обновление')
})
q.then(() => {
console.log('Второе обновление')
})
q.then(() => {
console.log('Третье обновление')
})
Смешивание колбеков и промисов может привести к дублированию операций.
Для валидации характерно следующее:
- Она определяется в
SchemaType
(при определении схемы) - Валидация - это посредник (middleware).
Mongoose
регистрирует ее как хукpre('save')
для каждой схемы - Автоматическую валидацию, выполняемую перед сохранением документа, можно отключить с помощью настройки
validateBeforeSave: false
- Валидаторы можно запускать вручную с помощью
doc.validate(callback)
илиdoc.validateSync()
- Помечать поля как невалидные можно с помощью
doc.invalidate(...)
- Валидаторы не запускаются для неопределенных значений, кроме валидатора
required
- Валидаторы выполняются асинхронно и рекурсивно: при вызове
Model.save()
также выполняется валидация субдокументов. При возникновении ошибки, ее получает колбекModel.save()
- Можно создавать собственные валидаторы
Mongoose
предоставляет несколько встроенных валидаторов:
- Все
SchemaTypes
имеют встроенный валидаторrequired
(поле является обязательным для заполнения) - Числа имеют валидаторы
min
иmax
- Строки имеют валидаторы
enum
,match
,minLength
иmaxLength
Значением валидатора является либо соответствующий примитив - условие, которому должно удовлетворять значение поля, либо массив, где первым элементом является примитив, а вторым - сообщение об ошибке
const breakfastSchema = new Schema({
eggs: {
type: Number,
min: [3, 'Слишком мало яиц'],
max: 6
},
bacon: {
type: Number,
required: [true, 'Без бекона?']
},
drink: {
type: String,
enum: ['Кофе', 'Чай'],
required: function () {
return this.bacon >= 2
}
}
})
const Breakfast = model('Breakfast', breakfastSchema)
const badBreakfast = new Breakfast({
eggs: 2,
bacon: 2,
drink: 'Молоко'
})
let error = badBreakfast.validateSync()
error.errors['eggs'].message // Слишком мало яиц
error.errors['drink'].message // `Молоко` is not a valid enum value for path `drink`
badBreakfast.drink = null
error = badBreakfast.validateSync()
error.errors['drink'].message // Path `drink` is required
badBreakfast.bacon = null
error = badBreakfast.validateSync()
error.errors['bacon'].message // Без бекона?
Обратите внимание, что настройка unique
не является валидатором. Это утилита для генерации уникальных индексов
const uniqueUsernameSchema = new Schema({
username: {
type: String,
unique: true
}
})
Кастомные валидаторы определяются следующим образом:
const userSchema = new Schema({
phone: {
type: String,
validate: {
validator: (v) => /+7\d{10}/.test(v),
message: props => `${props.value} не является валидным номером сотового телефона!`
},
required: [true, 'Номер телефона является обязательным']
}
})
const User = model('User', userSchema)
const user = new User()
user.phone = '3214256'
let error = user.validateSync()
error.errors['phone'].message // `3214256` не является валидным номером сотового телефона!
user.phone = ''
error = user.validateSync()
error.errors['phone'].message // Номер телефона является обязательным
Также существует два способа определения асинхронных кастомных валидаторов:
- с помощью
Promise.reject()
- с помощью
Promise.resolve(false)
const userSchema = new Schema({
name: {
type: String,
validate: () => Promise.reject(new Error('Упс!'))
},
email: {
type: String,
validate: {
validator: () => Promise.resolve(false),
message: 'Валидация email провалилась'
}
}
})
Если при выполнении валидации возникли ошибки, возвращается объект errors
, каждое значение которого представляет собой объект ValidatorError
(unique
возвращает DuplicateKeyError
). Объект ValidatorError
содержит свойства kind
, path
, value
и message
. Он также может содержать свойство reason
. Если в валидаторе было выброшено исключение, данное свойство будет содержать это исключение.
Перед запуском валидаторов Mongoose
выполняет проверку значений на соответствие типам. Данный процесс называется кастингом (casting). При провале кастинга, объект error.errors
будет содержать объект CastError
. Кастинг выполняется перед валидацией, при провале кастинга валидация не выполняется.
Для того, чтобы сделать вложенный объект обязательным, следует определить его в виде схемы
// Будет выброшено исключение, поскольку `name` не является самостоятельным путем
let userSchema = new Schema({
name: {
first: String,
last: String
},
required: true
})
const nameSchema = new Schema({
first: String,
last: String
})
// так будет работать
userSchema = new Schema({
name: {
type: nameSchema,
required: true
}
})
Mongoose
также поддерживает валидацию для операций update()
, updateOne()
, updateMany()
и findOneAndUpdate()
. По умолчанию такая валидацию отключена. Для того, чтобы ее включить, следует установить настройку runValidators
в значение true
const toySchema = new Schema({
color: {
type: String,
validate: (v) => /red|green|blue/i.test(v), 'Недопустимый цвет'
},
name: String
})
const Toy = model('Toy', toySchema)
const opts = { runValidators: true }
Toy.updateOne({}, { color: 'не цвет' }, opts, (err) => {
console.error(err) // Недопустимый цвет
})
Обратите внимание: валидаторы обновления имеют некоторые особенности, связанные с потерей контекста (this
), успешной валидацией несуществующих путей и возможностью их использования только в некоторых операциях.
Посредники (промежуточное программное обеспечение, промежуточный слой, middlewares), которые тажке называются пре и пост хуками, являются функциями, перехватывающими выполнение асинхронных функций. Посредники определяются на уровне схемы и часто используются для создания плагинов.
Mongoose
предоставляет 4 типа посредников: посредники документа, посредники модели, посредники агрегации и посредники запроса. В посредниках документов this
указывает на документ. Такие посредники поддерживаются для следующих функций документа:
validate
save
remove
updateOne
deleteOne
init
(хукиinit
являются синхронными)
В посредниках запросов this
указывает на запрос. Такие посредники поддерживаются для следующих функций моделей и запросов:
count
deleteMany
deleteOne
find
findOne
findOneAndDelete
findOneAndRemove
findOneAndUpdate
remove
update
updateOne
updateMany
Посредники агрегации выполняются при вызове call()
на агрегируемом объекте. В таких посредиках this
указывает на объект агрегации
aggregate
В посредниках модели this
указывает на модель. Поддерживаются следующие посредники модели:
insertMany
Обратите внимание: при определении schema.pre('remove')
автоматически регистрируется посредник для doc.remove()
. Для того, чтобы посредник выполнялся при вызове Query.remove()
, следует указать schema.pre('remove', { query: true, document: false }, fn)
.
Обратите внимание: в отличие от schema.pre('remove')
, Mongoose
по умолчанию автоматически регистрирует посредников для Query.updateOne()
и Query.deleteOne()
. Это означает, что doc.updateOne()
и Model.updateOne()
запускают хуки updateOne
, но this
указывает на запрос, а не на документ. Для регистрации updateOne
или deleteOne
в качестве посредников документа следует указать schema.pre('updateOne', { document: true, query: false })
.
Обратите внимание: функция create()
запускает хуки save()
.
Предварительные посредники выполняются один за другим, когда каждый посредник вызывает next()
const schema = new Schema(...)
schema.pre('save', (next) => {
// ...
next()
})
Вместо next()
можно использовать функцию, возвращающую промис, или async/await
schema.pre('save', () =>
doStuff().then(() => doMoreStuff())
)
// или
schema.pre('save', async () => {
await doStuff()
await doMoreStuff()
})
Обратите внимание: вызов next()
не останавливает выполнение функции.
Посредники могут использоваться для атомизации логики модели. Другие варианты:
- сложная валидация
- удаление зависимых документов (удаление постов пользователя при удалении самого пользователя)
- асинхронные настройки по умолчанию
- выполнение асинхронных задач
При возникновении ошибки в хуке, другие хуки или соответствующая функция не выполняются. Вместо этого, ошибка передается в колбек, а возвращенный промис отклоняется. Существует несколько способов обработки таких ошибок:
schema.pre('save', (next) => {
const err = new Error('Что-то пошло не так')
// При вызове `next()` с аргументом, предполагается, что данный аргумент
// является ошибкой
next(err)
})
schema.pre('save', () =>
// Также можно вернуть отклоненный промис
new Promise((res, rej) => {
reject(new Error('Что-то пошло не так'))
})
)
schema.pre('save', () => {
// или выбросить синхронное исключение,
throw new Error('Что-то пошло не так')
})
schema.pre('save', async () => {
await Promise.resolve()
// или выбросить исключение в асинхронной функции
throw new Error('Что-то пошло не так')
})
// Позже
// Изменения не будут сохранены в БД, поскольку в предварительном хуке возникает ошибка
doc.save((err) => {
console.error(err) // Что-то пошло не так
})
Последующие посредники выполняются после метода и всех его предварительных посредников
schema.post('init', (doc) => {
console.log('%s был получен из БД', doc._id)
})
schema.post('validate', (doc) => {
console.log('%s был проверен (но еще не сохранен)', doc._id)
})
schema.post('save', (doc) => {
console.log('%s был сохранен', doc._id)
})
schema.post('remove', (doc) => {
console.log('%s был удален', doc._id)
})
Если последующий хук вызывается с 2 аргументами, Mongoose
предполагает, что второй параметр - это функция next()
, предназначенная для вызова следующего посредника в цепочке
schema.post('save', (doc, next) => {
const timerId = setTimeout(() => {
console.log('post1')
// Запускаем второй хук
next()
clearTimeout(timerId)
}, 100)
})
// Не будет выполняться до вызова `next()` в первом посреднике
schema.post('save', (doc, next) => {
console.log('post2')
next()
})
Посредники и плагины должны определяться до компиляции модели посредством вызова mongoose.model()
.
Если определение схемы и экспорт модели выполняются в одном файле, глобальные плагины должны определяться перед вызовом require()
.
Функция save()
запускает хуки validate()
, поскольку Mongoose
имеет встроенный хук pre('save')
, вызывающий validate()
. Это означает, что все хуки pre('validate')
и post('validate')
вызываются перед хуками pre('save')
.
Для remove()
поддерживаются как хуки документов, так и хуки запросов.
Для переключения хука remove()
между Document.remove()
и Model.remove()
следует передать объект с настройками в Schema.pre()
или Schema.post()
// Только посредник документа
schema.pre('remove', { document: true, query: false }, () => {
console.log('Удаление документа')
})
// Только посредник запроса. Будет вызываться только для `Model.remove()`,
// но не для `doc.remove()`
schema.pre('remove', { query: true, document: false }, () => {
console.log('Удаление')
})
Выполнение посредника, обычно, останавливается при первом вызове next()
с ошибкой. Тем не менее, существует специальный тип последующих посредников - посредники для обработки ошибок, которые выполняются при возникновении ошибок. Такие посредники могут использоваться для вывода сообщений об ошибках в удобочитаемом формате.
Посредники для обработки ошибок определяются как посредники, в качестве первого параметра принимающие возникшую ошибку
const chema = new Schema({
name: {
type: String,
// При сохранении дубликата,
// будет выброшена `MongoError` с кодом 11000
unique: true
}
})
// Обработчик принимает три параметра: возникшую ошибку, документ
// и функцию `next()`
schema.post('save', (err, doc, next) => {
if (err.name === 'MongoError' && err.code === 11000) {
next(new Error('Попытка сохранения дубликата'))
} else {
next()
}
})
// Это запустит обработчик ошибок `post('save')`
User.create([{ name: 'John Smith' }, { name: 'John Smith' }])
Рассматриваемые посредники также работают с посредниками запросов. Мы может определить хук post('update')
, который будет перехватывать ошибки, связанные с дубликатами
schema.post('update', (err, res, next) => {
if (err.name === 'MongoError' && err.code === 11000) {
next(new Error('Попытка создания дубликата'))
} else {
next()
}
})
const users = [
{ name: 'John Smith' },
{ name: 'Jane Air' }
]
User.create(users, (err) => {
User.update({ name: 'Jane Air' }, { $set: { name: 'John Smith' } }, (err) => {
console.error(err) // Попытка создания дубликата
})
})
Функция populate()
позволяет ссылаться на документы из других коллекций.
Популяция (заполнение, population) - это процесс автоматической замены определенных путей в документе документами из других коллекций. Мы можем заполнять пути единичными документами, несколькими документами, единичным объектом, несколькими документами или всеми документами, возвращаемыми запросом. Рассмотрим пример
const { Schema, model } = require('mongoose')
const userSchema = new Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
posts: [
{
type: Schema.Types.ObjectId,
ref: 'Story'
}
]
})
const postSchema = new Schema({
author: {
type: Schema.Types.ObjectId,
ref: 'User'
},
title: String,
subscribers: [
{
type: Schema.Types.ObjectId,
ref: 'User'
}
]
})
const Post = model('Post', postSchema)
const User = model('User', userSchema)
У нас имеется две модели. У модели User
есть поле posts
, значением которого является массив ObjectId
. Настройка ref
сообщает Mongoose
, какую модель использовать в процессе популяции, в нашем случае такой моделью является Post
. Все сохраняемые здесь _id
должны быть _id
документов из модели Post
.
Обратите внимание: в качестве ссылок могут использоваться ObjectId
, String
, Number
и Buffer
. Однако, по возможности всегда следует использовать ObjectId
.
Сохранение ссылок похоже на сохранение обычных свойств: достаточно присвоить значение _id
const author = new User({
_id: new mongoose.Types.ObjectId(),
name: 'John Smith',
age: 30
})
author.save((err) => {
if (err) return handleError(err)
const post1 = new Post({
title: 'Мой первый пост',
author: author._id // присваиваем `_id` из `author`
})
post1.save((err) => {
if (err) return handleError(err)
// готово
})
})
Заполним поле author
поста с помощью строителя запроса
Post
.findOne({ title: 'Мой первый пост' })
.populate('author')
.exec((err, post) => {
if (err) return handleError(err)
console.log('Автором поста является %s', post.author.name)
// Автором поста является John Smith
})
Post.findOne({
title: 'Мой первый пост'
}, (err, post) => {
if (err) return handleError(err)
post.author = author
console.log(post.author.name) // John Smith
})
Для определения заполняемости поля используется функция populated()
post.populated('author') // truthy
post.depopulate('author')
post.populated('author') // undefined
Это может использоваться для получения id
автора. Однако, специально для таких случаев Mongoose
предоставляет геттер _id
, позволяющий получать идентификатор независимо от заполняемости поля
post.populated('author') // truthy
post.author._id // ObjectId
post.depopulate('author')
post.populated('author') // undefined
post.author._id // ObjectId, это возможно благодаря специальному геттеру
В этом случае post.author
будет иметь значение null
await User.deleteOne({ name: 'John Smith' })
const post = await User.findOne({ title: 'Мой первый пост' }).populate('author')
post.author // null
При определении authors
в postSchema
в виде массива, populate()
вернет пустой массив.
Для выполнения выборки достаточно передать методу populate()
строку в качестве второго аргумента
Post
.findOne({ title: 'Мой первый пост' })
.populate('author', 'name') // вернется только имя автора
.exec((err, post) => {
if (err) return handleError(err)
console.log('Автора зовут %s', post.author.name)
// Автора зовут John Smith
console.log('Автору %s лет', post.author.age)
// Автору null лет
})
Post
.find()
.populate('author')
.populate('subscribers')
.exec()
Если для одного пути вызывается несколько populate()
, эффект будет иметь только последний из них.
Что если мы хотим заполнить массив подписчиков на основе их возраста и только их именами?
Post
.find()
.populate({
path: 'subscribers',
match: { age: { $gte: 21 } }, // больше или равно, greater than or equal
// явно исключаем `_id`
select: 'name -_id'
}).exec()
При отсутствии совпадения, массив subscribers
будет пустым.
const post = await Post
.findOne({ title: 'Мой первый пост' })
.populate({
path: 'author',
name: {
// не равно, not equal
$ne: 'John Smith'
}
}).exec()
post.author // null
Для ограничения количество возвращаемых объектов можно использовать настройку perDocumentLimit
(настройка options.limit
работает не всегда корректно)
Post.create([
{
title: 'Post1',
fans: [1, 2, 3, 4, 5]
},
{
title: 'Post2',
fans: [6, 7]
}
])
const posts = Post
.find()
.populate({
path: 'fans',
options: {
limit: 2
}
})
posts[0].name // Post1
posts[0].fans.length // 2
// !
posts[1].name // Post2
posts[1].fans.length // 0
const posts = await Post
.find()
.populate({
path: 'subscribers',
perDocumentLimit: 2
})
posts[0].name // Post1
posts[0].fans.length // 2
// !
posts[1].name // Post2
posts[1].fans.length // 2
Мы можем обнаружить, что при использовании объекта author
, нам недоступен список постов. Это объясняется тем, что объекты story
не были помещены (pushed) в author.posts
.
Если у нас имеется хорошая причина для сохранения массива дочерних указателей (child pointers), мы можем push()
документы в массив
author.posts.push(post1)
author.save()
Это позволит комбинировать find()
и populate()
User
.findOne({ name: 'John Smith' })
.populate('posts')
.exec((err, user) => {})
Возможно, мы не хотим иметь два набора указателей. В этом случае мы можем пропустить популяцию и найти нужные посты с помощью find()
Post
.find({ author: author._id })
.exec((err, posts) => {})
Для заполнения полей существующего документа используется цепочка из методов populate()
и Document.execPopulate()
const user = await User.findOne({ name: 'John Smith' })
user.populated('posts') // null
await user.populate('posts').execPopulate()
user.populated('posts') // массив `ObjectId`
user.posts[0].name // Post1
Несколько вызовов populate()
могут быть объединены в цепочку.
Предположим, что у нас имеется схема пользователя, которая "следит" за друзьями пользователя
const userSchema = new Schema({
name: String,
friends: [
{
type: ObjectId,
ref: 'User'
}
]
})
Допустим, что мы хотим получать не только друзей пользователя, но и друзей друзей пользователя
User
.findOne({ name: 'Имярек' })
.populate({
path: 'friends',
// !
populate: {
path: 'friends'
}
})
Предположим, что у нас имеется схема событий и схема обсуждений. Каждое событие имеет соответствующий поток (thread) обсуждений
const db1 = mongoose.createConnection(MONGO_URI1)
const db2 = mongoose.createConnection(MONGO_URI2)
const conversationSchema = new Schema({ messagesCount: Number })
const Conversation = db2.model('Conversation', conversationSchema)
const eventSchema = new Schema({
name: String,
conversation: {
type: ObjectId,
ref: Conversation // `ref` - это класс модели, а не строка
}
})
const Event = db1.model('Event', eventSchema)
const events = await Event
.find()
.populate('conversations')
// или когда мы не имеем доступа к `Conversation` при определении `eventSchema`
const events = await Event
.find()
.populate('conversations', model: Conversation)
Mongoose
также может осуществлять заполнение из нескольких коллекций на основе значения свойства документа. Предположим, что у нас имеется схема для комментариев. Пользователь может оставлять комментарии как к посту, так и к товару
const commentSchema = new Schema({
body: {
type: String,
required: true
},
on: {
type: Schema.Types.ObjectId,
required: true,
// Для определения правильной модели `Mongoose` будет использовать свойства `onModel`
refPath: 'onModel'
},
onModel: {
type: String,
required: true,
enum: ['Post', 'Product']
}
})
const Product = model('Product', new Schema({ name: String }))
const Post = model('Post', new Schema({ title: String }))
const Comment = model('Comment', commentSchema)
Другими словами, refPath
позволяет определить, какую модель Mongoose
должен использовать для каждого документа
const book = await Product.create({ name: 'Book1' })
const post = await Post.create({ title: 'Post1' })
const commentOnBook = await Comment.create({
body: 'Comment1',
on: book._id,
onModel: 'Product'
})
const commentOnPost = await Comment.create({
body: 'Comment2',
on: post._id,
onModel: 'Post'
})
// `populate()` работает, несмотря на то, что один комментарий
// ссылается на коллекцию `Product`, а другой - на коллекцию `Post`
const comments = await Comment.find().populate('on').sort({ body: 1 })
comments[0].on.name // Book1
comments[1].on.title // Post1
Виртуальные свойства позволяют определять более сложные или тонкие отношения между документами
const personSchema = new Schema({
name: String,
band: String
})
const bandSchema = new Schema({
name: String
})
bandSchema.virtuals('members', {
ref: 'Person', // Модель, которую следует использовать
localField: 'name', // Находим людей, у которых `localField`
foreignField: 'band', // равняется `foreignField`
// Если `justOne` равняется `true`, `members` будет единичным документом,
// а не массивом. По умолчанию `justOne` имеет значение `false`
justOne: false,
options: {
sort: {
name:
},
limit: 5
} // настройки запроса
})
const Person = model('Person', personSchema)
const Band = model('Band', bandSchema)
/**
* Предположим, что у есть две группы: `Guns N' Roses` и `Motley Crue`
* и 4 человека: `Axl Rose` и `Slash` в `Guns N' Roses` и
* `Vince Neil` и `Nikki Sixx` в `Motley Crue`
*/
Band
.find({})
.populate('members')
.exec((err, bands) => {
// `bands.members` теперь является массивом экземпляров `Person`
})
Для дополнительной фильтрации результатов populate()
можно использовать настройку match
const personSchema = new Schema({
name: String,
band: String,
isActive: Boolean
})
const bandSchema = new Schema({
name: String
})
bandSchema.virtual('activeMembers', {
ref: 'Person',
localField: 'name',
foreignField: 'band',
justOne: false,
match: { isActive: true }
})
bandSchema.virtual('formerMembers', {
ref: 'Person',
localField: 'name',
foreignField: 'band',
justOne: false,
match: { isActive: false }
})
Для получения количества совпавших с foreignField
документов используется настройка count
const personSchema = new Schema({
name: String,
band: String
})
const bandSchema = new Schema({
name: String
})
bandSchema.virtual('numMembers', {
ref: 'Person',
localField: 'name',
foreignField: 'band',
count: true
})
// Позже
const doc = await Band
.findOne({ name: 'Motley Crue' })
.populate('numMembers')
doc.numMembers // 2
Карты - тип, представляющий собой объект с произвольными строковыми ключами
const bandSchema = new Schema({
name: String,
members: {
type: Map,
of: {
type: ObjectId,
ref: 'Person'
}
}
})
const Band = model('Band', bandSchema)
Карта имеет ref
, что означает, что мы можем использовать populate()
для ее заполнения. Допустим, у нас есть такой документ 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
}
})
Заполнение карты выполняется с помощью специального синтаксиса $*
const band = await Band.findOne({ name: 'Motley Crue' }).populate('members.$*')
band.members.get('signer') // { _id: ..., name: 'Vince Neil' }
Мы также можем заполнять поля карт субдокументов
const librarySchema = new Schema({
name: String,
books: {
type: Map,
of: new Schema({
title: String,
author: {
type: 'ObjectId',
ref: 'Person'
}
})
}
})
const Library = model('Library', librarySchema)
const libraries = await Library.find().populate('books.$*.author')
// Всегда выполняем заполнение при поиске
MySchema.pre('find', function() {
this.populate('user')
})
// Выполняем заполнение после поиска
MySchema.post('find', async (docs) => {
for (const doc of docs) {
if (doc.isPublish) {
await doc.populate('user').execPopulate()
}
}
})
// Выполняем заполнение после сохранения
MySchema.post('save', (doc, next) => {
doc.populate('user').execPopulate().then(() => {
next()
})
})
Mongoose()
- конструкторMongoose
const mongoose = require('mongoose')
mongoose instanceof mongoose.Mongoose // true
Mongoose.model()
- метод для определения или извлечения модели
const { model } = require('mongoose')
module.exports = model('MyModel', schema)
Mongoose.Schema()
- конструктор схемы
const { Schema } = require('mongoose')
const mySchema = new Schema({...})
Mongoose.connect()
- метод для подключения кMongoDB
const mongoose = require('mongoose')
const MONGO_URI = require('./config')
const options = {
useNewUrlParser: true,
useUnifiedTopology: true
}
// Выполняем подключение с минимальными настройками
mongoose.connect(
MONGO_URI,
options,
(err) => {
if (err) {
console.error('Something went wrong: ', err.message || err)
return
}
console.log('The connection to the database is successful')
})
Mongoose.connection
- текущее подключение, эквивалентmongoose.connections[0]
mongoose.connect(...)
mongoose.connection.on('error', (err) => handleError(err))
-
Mongoose.connections
- массив всех подключений -
Mongoose.createConnection()
- синхронный метод для создания подключения, который используется для управления несколькими подключениями
const db1 = mongoose.createConnection(MONGO_URI1, options)
const db2 = mongoose.createConnection(MONGO_URI2, options)
-
Mongoose.disconnect()
- запускаетclose()
на всех соединениях одновременно -
Mongoose.plugin()
- метод для определения глобальных плагинов -
Mongoose.get()
,Mongoose.set()
- методы для получения и установки глобальных настроек
mongoose.set('debug', true)
Schema()
- конструктор схемы (Mongoose.Schema()
)
const { Schema } = require('mongoose')
// автоматическое создание полей `createdAt` и `updatedAt`
const options = {
timestamps: true
}
const userSchema = new Schema({
name: {
type: String,
trim: true,
minLength: [2, 'Имя слишком короткое'],
maxLength: [12, 'Имя слишком длинное'],
required: [true, 'Имя не может быть пустым']
},
age: {
type: Number,
min: [18, 'Ты слишком юн'],
max: [66, 'Ты слишком стар'],
required: true,
default: 18
},
email: {
type: String,
trim: true,
lowercase: true,
match: [/\w+@\w+\.\w+/, 'Неправильный адрес электронной почты'],
requred: [true, 'Email не может быть пустым'],
unique: true
},
sex: {
type: String,
enum: ['мужской', 'женский', 'не могу определиться'],
required: true
},
phone: {
type: String,
validate: {
validator: (v) => /+7\d{10}/.test(v),
message: () => 'Неправильный номер телефона'
},
required: true
},
agree: {
type: Boolean,
required: true
}
}, options)
-
Schema.Types
- встроенные типы схем:String
Number
Boolean
,Array
,Buffer
Date
,ObjectId
Mixed
-
Schema.add()
- метод для добавления полей в схему
parentSchema.add(childSchema).add({ color: String })
-
Schema.clone()
- метод для глубокого копирования схемы -
Schema.eachPath()
- метод для перебора полей (аналогforEach()
)
schema.eachPath((pathname, schematype) => {
console.log(pathname, schematype)
})
Schema.method()
- позволяет добавлять методы в схему
const userSchema = new Schema({
name: {
first: String,
last: String
}
})
userSchema.method('fullName', function() {
return `${this.name.first} ${this.name.last}`
})
const User = model('User', userSchema)
const user = new User({
name: {
first: 'John',
last: 'Smith'
}
})
console.log(user.fullName()) // John Smith
Schema.path()
- метод для получения/установки путей (типов полей)
schema.path('name') // возвращается тип поля `name`
schema.path('name', Number) // типом `name` теперь является `Number`
-
Schema.post()
- метод для определения последующих хуков -
Schema.pre()
- метод для определения предварительных хуков -
Schema.remove()
- метод для удаления поля или полей в случае передачи массива -
Schema.get()
,Schema.set()
- методы для получения и установки настроек схемы -
Schema.static()
- позволяет добавлять статические методы в схему
schema.static('findByName', function(name) {
return this.find({ name })
})
const MyModel = model('MyModel', schema)
await MyModel.findByName('Anonymous')
Schema.virtual()
- метод для создания виртуальных типов
Model()
- класс, который используется для взаимодействия сMongoDB
. Экземпляры этого класса называются документами
const { Schema, model } = require('mongoose')
const userSchema = new Schema({...})
const User = model('User', userSchema)
const newUser = new User({...})
// сохраняем пользователя в БД
await newUser.save()
// получаем пользователя из БД
const user = await User.findOne({...})
-
Model.bulkWrite()
- позволяет отправить сразу несколько операций (переданных в виде массива) вMongoDB
. Поддерживаемые операции:insertOne
updateOne
updateMany
deleteOne
deleteMany
replaceOne
-
Model.estimatedDocumentCount()
- возвращает количество документов, удовлетворяющих условию
User.estimatedDocumentCount({ type: 'vip' }, (err, count) => {})
Model.create()
- метод для сохранения одного или нескольких документов в БД. Данный метод запускаетsave()
(и его посредников) для каждого документа
await User.create({ name: 'John Smith' })
await User.create([
{ name: 'John Smith' },
{ name: 'Jane Air' }
])
Model.deleteMany()
- удаляет из коллекции все документы, удовлетворяющие условию. Похож наremove()
, но удаляет все документы независимо от настройкиsingle
Model.deleteMany(conditions, options?, callback?)
await User.deleteMany({ name: /john/i, age: { $lte: 18 } })
Model.deleteOne(conditions, options?, callback?)
- удаляет из коллекции первый документ, удовлетворяющий условию
await User.deleteOne({ name: 'John Smith' })
Model.events
- диспетчер событий, который может использоваться для глобальной обработки ошибок
Model.events.on('error', err => handleError(err))
Model.find()
- метод для поиска документов
Model.find(filter, projection?, options?, callback?)
// все документы
await User.find({})
// всех пользователей с именем `John`, старше 18
await User.find({ name: 'John', age: { $gt: 18 } }).exec()
// с результатами в колбеке
await User.find({ name: 'John', age: { $gt: 18 } }, (err, docs) => {})
// регулярное выражение и выборка полей
await User.find({ name: /john/i }, 'name friends').exec()
// с настройками
await User.find({ name: /john/i }, null, { skip: 10 }).exec()
Model.findById()
- метод для поиска документа по_id
.findById(id)
почти идентиченfindOne({ _id: id })
Model.findById(filter, projection?, options?, callback?)
await User.findById(id).exec()
await User.findById(id, (err, user) => {})
await User.findById(id, 'name friends').exec()
-
Model.findByIdAndDelete(id, options?, callback?)
- метод для поиска и удаления документа по_id
.findByIdAndDelete(id)
является сокращением дляfindOneAndDelete({ _id: id })
-
Model.findByIdAndRemove(id, options?, callback?)
- метод для поиска и удаления документа по_id
.findByIdAndRemove(id, ...)
является сокращением дляfindOneAndRemove({ _id: id }, ...)
. Данный метод находит документ, удаляет его и передает обнаруженный документ в колбек -
Model.findByIdAndUpdate(id, update, options?, callback?)
- метод для поиска и обновления документа по_id
.findByIdAndUpdate(id, ...)
является сокращением дляfindOneAndUpdate({ _id: id }, ...)
. Данный метод находит документ, обновляет его согласно аргументуupdate
, применяетoptions
и передает обнаруженный документ в колбек. Важные настройки:new
- еслиtrue
, возвращает обновленный документ, а не исходныйupsert
- еслиtrue
, объект создается при отсутствии
await User.findByIdAndUpdate(id, { name: 'Jane Air' }, { new: true }, (err, user) => {})
-
Model.findOne(conditions?, projection?, options?, callback?)
- метод для поиска документа по условию. Объект с условиями является опциональным. Однако, если он имеет значениеnull
илиundefined
, возвращается произвольный документ. Для выполнения поиска документа по_id
следует использоватьfindById()
-
Model.findOneAndDelete(conditions, options?, callback?)
- метод для поиска и удаления документа по условию. Данный метод находит документ, удаляет его и передает обнаруженный документ в колбек -
Model.findOneAndRemove(conditions, options?, callback?)
- метод для поиска и удаления документа. Почти идентиченModel.findOneAndDelete()
-
Model.findOneAndReplace(filter, replacement?, options?, callback?)
- метод для поиска и замены документа по условию. Данный метод находит документ, заменяет его и передает обнаруженный документ в колбек. Для передачи в колбек обновленного документа следует установить настройкуnew
в значениеtrue
-
Model.findOneAndUpdate(conditions?, update?, options?, callback?)
- метод для поиска и обновления документа по условию. Данный метод находит документ, обновляет его согласно аргументуupdate
, применяетoptions
и передает обнаруженный документ в колбек. Важные настройки:new
- см.findByIdAndUpdate()
upsert
- см.findByIdAndUpdate()
overwrite
- еслиtrue
, документ заменяется, а не обновляетсяfields
- выборка полей, эквивалентselect(fields).findOneAndUpdate()
-
Model.init()
- метод для генерации индексов приautoIndex: false
-
Model.insertMany(doc(s), options?, callback?)
- метод для добавления единичного документа или массива документов в БД. Все добавляемые документы должны быть валидными (если не установленоordered: false
). Данный метод быстрееcreate()
, поскольку отправляет в БД всего одну операцию (create()
отправляет операцию для каждого документа)
const users = [
{ name: 'John' },
{ name: 'Jane' }
]
User.insertMany(users, (err, users) => {})
Model.populate(doc(s), options, callback?)
- метод для заполнения ссылок единичного документа или массива документов документами из других коллекций.options
- либо строка с указанием пути, либо объект с настройками:path
- пути для заполненияselect
- выборка полейmatch
- условияmodel
- название модели для популяцииoptions
-sort
,limit
и т.д.justOne
// Заполнение единичного объекта
User.findById(id, (err, user) => {
const opts = [
{
path: 'company',
match: { x: 1 },
select: 'name'
},
{
path: 'notes',
options: { limit: 10 },
model: 'override'
}
]
User.populate(user, opts, (err, user) => {
console.log(user)
})
})
// Заполнение массива объектов
User.find(match, (err, users) => {
const opts = [
{
path: 'company',
match: { x: 1 },
select: 'name'
}
]
User.populate(users, opts).then(console.log).end()
})
Model.remove(conditions, options?, callback?)
- метод для удаления документов из коллекции по условию. Для удаления первого совпавшего документа следует установитьsingle: true
const result = await User.remove({ name: /john/i })
result.deletedCount // количество удаленных документов
-
Model.replaceOne(filter, doc, options?, callback?)
- тоже самое, что иupdate()
, за исключением того, что существующий документ полностью заменяется переданным (не допускается атомарных операций типа$set
) -
Model.update(filter, doc, options?, callback?)
- метод для обновления документа без его возвращения. Важные настройки:upsert
- см.findByIdAndUpdate()
multi
- позволяет обновлять несколько документовoverwrite
- см.findOneAndUpdate()
await User.update({ age: { $gt: 18 } }, { oldEnough: true }, (err, rawResponse) => {})
-
Model.updateOne(filter, doc, options?, callback?)
- тоже самое, чтоupdate()
, но без настроекmulti
иoverwrite
-
Model.updateMany(filter, docs, options?, callback?)
- тоже самое, чтоupdate()
, но обновляются все документы, удовлетворяющие условию, независимо от настройкиmulti
-
Model.watch(pipeline?, options?)
- метод, позволяющий следить за изменениями соответствующей коллекции. Можно регистрировать следующие события:change
error
end
close
const user = await User.create({ name: 'John Smith' })
const changeStream = User.watch().on('change', change => console.log(change))
// Будет выведено следующее:
// { _id: { _data: ... },
// operationType: 'delete',
// ns: { db: '...', coll: 'User' },
// documentKey: { _id: ... } }
await user.remove()
Model.where(path, val?)
- позволяет формировать запрос, передавать ему условия и возвращать его. Например, вместо
User.find({ age: { $gte: 18, $lte: 66 } }, callback)
Можно делать так
User.where('age').gte(18).lte(66).exec(callback)
Document.depopulate(path)
- принимает заполняемое поле и возвращает его к незаполненному состоянию
Post.findOne().populate('author').exec((err, post) => {
console.log(post.author.name) // John Smith
post.depopulate('author')
console.log(doc.author) // ObjectId
})
-
Document.equals()
- возвращаетtrue
, если документ равен другому документу. Документы сравниваются по_id
. Если документы не имеют_id
, используется функцияdeepEqual()
-
Document.execPopulate()
- метод для явного выполнения популяции, возвращающий промис. Используется для интеграции промисов
const promise = doc
.populate('company')
.populate({
path: 'notes',
match: /airlane/,
select: 'text',
model: Model,
options: opts
})
.execPopulate()
Вместо doc.populate(options).execPopulate()
можно использовать doc.execPopulate(options)
.
-
Document.get(path, type?, options?)
- возвращает значение пути -
Document.id
- строковое представление_id
документа -
Document.isNew
- индикатор новизны документа -
Document.markModified(path)
- помечает путь как имеющий ожидающие изменения для записи в БД. Обычно, применяется в отношении смешанных типов
doc.mixed.type = 'changed'
doc.markModified('mixed.type')
doc.save()
-
Document.overwrite(obj)
- перезаписывает значения документа значениямиobj
, кроме иммутабельных свойств. Похож наset()
, но те свойства, которых нет вDocument.obj
, удаляются -
Document.parent()
- если документ является субдокументом, возвращается его предок -
Document.populate(path?, callback?)
- заполняет ссылки документа, выполняет колбек после завершения. Для того, чтобы получить промис, данный метод следует использовать совместно сexecPopulate()
doc
.populate('company')
.populate({
path: 'notes',
match: /airline/,
select: 'text',
model: Model,
options: opts
}, (err, user) => {
console.log(doc._id === user._id) // true
})
Обратите внимание: популяция не выполняется до передачи колбека или вызова execPopulate()
.
Document.save(options?, callback?)
- сохраняет документ в БД или отправляет операциюupdateOne()
с изменениями
product.sold = Date.now()
await product.save()
-
Document.schema
- схема документа -
Document.set(path, val, type?, options?)
- устанавливает значение пути или нескольких путей
doc.set(path, value)
doc.set({
path: value,
path2: {
path: value
}
})
validate(path(s)?, options?, callback?)
иvalidateSync(path(s), options?)
- выполняют валидацию документа.validateSync()
пропускает асинхронные валидаторы
doc.validate((err) => {})
Query(options?, model?, conditions?, collection?)
- конструктор для построения запросов
const query = Model.find()
query.setOptions({ lean: true })
query.collection(Model.collection)
query.where('age').gte(18).exec(callback)
Query.all(path?, val)
- определяет условие запроса$all
Model.find().where('pets').all(['dog', 'cat'])
// или
Model.find().all('pets', ['dog', 'cat'])
Query.and(array)
- определяет аргументы для условия$and
query.and([{ color: 'green' }, { status: 'ok' }])
Query.circle(path?, area)
- определяет условия$center
или$centerSphere
const area = { center: [50, 50], radius: 10, unique: true }
query.where('loc').within().circle(area)
// или
query.circle('loc', area)
// сферические вычисления
const area = { center: [50, 50], radius: 10, unique: true, spherical: true }
query.circle('loc', area)
-
Query.exec(operation?, callback?)
- выполняет запрос -
Query.get(path)
- для операций обновления. Возвращает значение пути в обновлении$set
const query = Model.updateOne({}, { $set: { name: 'John Smith' } })
query.get('name') // John Smith
-
Условия запросов (
$condition
):gt()
- больше чемgte()
- больше или равноlt()
- меньше чемlte()
- меньше или равноin()
- диапазон,enum
nin()
- противоположностьin()
or()
- или то, или другоеnor()
- ни то, ни другоеne()
- не равноregex()
- регулярное выражениеsize()
- размерslice()
- срез, диапазон
-
Query.limit()
- лимит возвращаемых документов -
Query.merge()
- объединяет запросы -
Query.near()
- определяет условие$near
или$nearSphere
query.where('loc').near({ center: [10, 10] })
query.where('loc').near({ center: [10, 10], maxDistance: 5 })
query.where('loc').near({ center: [10, 10], maxDistance: 5, apherical: true })
query.near('loc', { center: [10, 10], maxDistance: 5 })
-
Query.post(callback)
- добавляет последующих посредников -
Query.pre(callback)
- добавляет предварительных посредников -
Query.select(arg)
- определяет, какие поля документа должны включаться, а какие не должны. Флаг-
означает исключение поля, флаг+
- принудительное включение (например, для полей, исключенных на уровне схемы)
// включить поля `a` и `b`, исключив остальные
query.select('a b')
query.select(['a', 'b'])
query.select({ a: 1, b: 1 })
// исключить поля `c` и `d`
query.select('-c -d')
Query.set(path, val?)
- добавляет$set
в запрос на обновление без изменения операции
query.updateOne({}, {}).set('updatedAt', new Date())
query.updateMany({}, {}).set({ 'updatedAt', new Date() })
Query.skip(val)
- определяет количество пропускаемых документов
query.skip(10).limit(5)
Query.sort(arg)
- определяет порядок сортировки (asc
,desc
,ascending
,descending
,1
,-1
)
query.sort({ field: 'asc', test: -1 })
// или
query.sort('field -test')
Query.where(path?, val?)
- определяет путь для выполнения запросов
// вместо
User.find({ age: { $gte: 18, $lte: 65 } }, callback)
// можно писать так
User.where('age').gte(18).lte(65)