Mongoose
Mongoose - это ORM (Object Relational Mapping - объектно-реляционное отображение или связывание) для
MongoDB
.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: ... }
SchemaTypes
Что такое SchemaType?
Схема представляет собой объект конфигурации для модели. 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
Для определения свойства с названием 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
ObjectId
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()
})
Mixed (смешанный тип)
Mongoose
не осуществляет проверку смешанных типов
new Schema({ any: {} })
new Schema({ any: Object })
new Schema({ any: Schema.Types.Mixed })
new Schema({ any: mongoose.Mixed })
При изменении значений смешанного типа, их, как и даты, необходимо помечать с помощью markModified
.
ObjectId
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
Map
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()