Mongoose - это ORM (Object Relational Mapping - объектно-реляционное отображение или связывание) для MongoDB. Mongoose предоставляет в распоряжение разработчиков простое основанное на схемах решение для моделирования данных приложения, включающее встроенную проверку типов, валидацию, формирование запросов и хуки, отвечающие за реализацию дополнительной логики обработки запросов.
catSchema.methods.speak=function(){constgreet=this.name?`Меня зовут ${this.name}`:`У меня нет имени`;console.log(greet);};constkitty=newCat({name:'Cat'});kitty.speak();// Меня зовут Cat
Mongoose позволяет определять вторичные индексы в Schema на уровне пути или на уровне schema
1234567
constcatSchema=newSchema({name:String,type:String,tags:{type:[String],index:true},// уровень поля});catSchema.index({name:1,type:-1});// уровень схемы
При запуске приложения для каждого индекса вызывается createIndex(). Индексы полезны для разработки, но в продакшне их лучше не использовать. Отключить индексацию можно с помощью autoIndex: false
// СхемаconstpersonSchema=newSchema({name:{first:String,last:String,},});// МодельconstPerson=model('Person',personSchema);// Документconstjohn=newPerson({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 Smithjohn.fullName='Jane Air';john.fullName;// Jane Air
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
Схема представляет собой объект конфигурации для модели. SchemaType - это объект конфигурации для определенного свойства модели. SchemaType определяет тип, геттеры, сеттеры и валидаторы свойства.
При изменении даты, например, с помощью метода setMonth(), ее необходимо пометить с помощью markModified(), иначе, изменения не будут сохранены
1 2 3 4 5 6 7 8 910
constschema=newSchema('date',{dueDate:Date});schema.findOne((err,doc)=>{doc.dueDate.setMonth(3);// это не сработаетdoc.save();// надо делать такdoc.markModified('dueDate');doc.save();});
ObjectId - это класс, а сами ObjectIds - это объекты, которые, обычно, представлены в виде строки. При преобразовании ObjectId с помощью метода toString(), мы получаем 24-значную шестнадцатиричную строку
Геттеры похожи на виртуальные свойства для полей схемы. Предположим, что мы хотим сохранять аватар пользователя в виде относительного пути и затем добавлять к нему название хоста в приложении
За счет буферизации Mongoose позволяет использовать модели, не дожидаясь подключения к БД. Отключить буферизацию можно с помощью bufferCommands: false. Отключить ожидание создания коллекций можно с помощью autoCreate: false
Существует 2 класса ошибок, возникающих при подключении к БД:
Ошибка при первоначальном подключении. При провале подключения Mongoose отправляет событие error и промис, возвращаемый connect(), отклоняется. При этом, Mongoose не пытается выполнить повторное подключение
Ошибка после установки начального соединения. В этом случае Mongoose пытается выполнить повторное подключение
Для обработки первого класса ошибок используется catch() или try/catch
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
mongoose.connect('mongodb://<username>:<password>@host1.com:27017,host2.com:27017,host3.com:27017/testdb');// к одному узлуmongoose.connect('mongodb://host1.com:port1/?replicaSet=rsName');
Модели - это конструкторы, компилируемые из определений Schema. Экземпляр модели называется документом. Модели отвечают за создание и получение документов из БД.
constUser=model('User',userSchema);constuser=newUser({name:'John',age:30});user.save((err)=>{if(err)returnhandleError(err);console.log(user);});// илиUser.create({name:'John',age:30},(err,user)=>{if(err)returnhandleError(err);console.log(user);});// или для создания нескольких документовUser.insertMany([{name:'John',age:30,},{name:'Jane',age:20,},],(err)=>{});
Для обновления документов используется метод updateOne() и др.
123456789
User.updateOne({name:'John'},{name:'Bob'},(err,res)=>{// Обновляется как минимум один документ// `res.modifiedCount` содержит количество обновленных документов// Обновленные документы не возвращаются});
Если мы хотим обновить один документ и вернуть его, то следует использовать findOneAndUpdate().
Поток изменений позволяет регистрировать добавление/удаление и обновление документов
1 2 3 4 5 6 7 8 910111213141516
construn=async()=>{constuserSchema=newSchema({name:String,});constUser=model('User',userSchema);// Создаем поток изменений// При обновлении БД отправляется (emitted) событие `change`User.watch().on('change',(data)=>console.log(newDate(),data));console.log(newDate(),'Создание документа');awaitUser.create({name:'John Smith'});console.log(newDate(),'Документ успешно создан');};
Документы - это экземпляры моделей. Документ и модель - разные классы Mongoose. Класс модели является подклассом класса документа. При использовании конструктора модели создается новый документ.
Документы подвергаются кастингу и валидации перед сохранением. Кастинг представляет собой проверку типа, а валидация - проверку дополнительных настроек (например, min для типа Number). Внутренне Mongoose вызывает метод документа validate() перед сохранением
1 2 3 4 5 6 7 8 9101112
constuserSchema=newSchema({name:String,age:{type:Number,min:18},});constUser=model('User',userSchema);constuser=newUser({name:'John',age:'many'});// Кастинг типа `Number` провален для значения `many` поля `age`awaituser.validate();constuser2=newUser((name:'Jane'),(age:17));// Значение поля `age` (17) меньше минимально допустимого значения (18)
Субдокументы похожи на обычные документы. Вложенные схемы могут иметь посредников, кастомную логику валидации, виртуальные свойства и т.д. Главное отличие субдокументов состоит в том, что они не сохраняются индивидуально, они сохраняются вместе с родительским документом.
123456789
constParent=model('Parent',parentSchema);constparent=newParent({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 будут выведены по порядкуconstchildSchema=newSchema({name:String});childSchema.pre('validate',(next)=>{console.log(2);next();});childSchema.pre('save',(next)=>{console.log(3);next();});constparentSchema=newSchema({child:childSchema,});parentSchema.pre('validate',(next)=>{console.log(1);next();});parentSchema.pre('save',(next)=>{console.log(4);next();});
Вложенные пути немного отличаются от субдокументов. В приведенном ниже примере у нас имеется две схемы: одна с субдокументом child, другая с вложенным путем child
экземпляры Nested никогда не имеют child === undefined. Мы может определять подсвойства child даже при отсутствии самого child. Но экземпляры Subdoc могут иметь child === undefined
Пути субдокументов имеют значение undefined (к ним не применяются настройки default) до тех пор, пока им не будет присвоено ненулевое значение
1 2 3 4 5 6 7 8 91011121314
constsubdocSchema=newSchema({child:newSchema({name:String,age:{type:Number,default:18,},}),});constSubdoc=model('Subdoc',subdocSchema);// настройка `default` поля `age` не будет иметь эффекта, поскольку значением `child` является `undefined`constdoc=newSubdoc();doc.child;// undefined
У каждого субдокумента есть метод remove(). Для массива субдокументов данный метод эквивалентен вызову pull(). Для единичного субдокумента он эквивалентен установке значения субдокумента в null
Для получения непосредственного предка субдокумента используется функция parent(), а для получения предка верхнего уровня (для глубоко вложенных субдокументов) - функция ownerDocument().
Обратите внимание, что в случае единичного объекта, вместо схемы создается вложенных путь. Для того, чтобы сообщить Mongoose о необходимости создания схемы для единичного объекта, следует использовать настройку typePojoToMixed: false
При выполнении запроса с помощью callback, запрос может быть определен в виде JSON документа
1 2 3 4 5 6 7 8 91011
constUser=model('User',userSchema);// Ищем пользователей, фамилия которых совпадает с `Smith`, выбираем поля `name` и `occupation`User.findOne({'name.last':'Smith'},'name occupation',(err,user)=>{if(err)returnhandleError(err);console.log(user);});
Запрос выполняется, его результат передается в callback. Сигнатура колбеков всегда такая: callback(error, result). При возникновении ошибки, параметр error содержит объект ошибки, а result является нулевым. При успешном выполнении запроса error является нулевым, а result заполняется (populate) данными из результата запроса.
result зависит от операции: для findOne() - это потенциально ненулевой единичный документ, для find() - список документов, для count() - количество документов, для update() - количество обновленных документов и т.д.
Пример без колбека
1 2 3 4 5 6 7 8 91011
// Ищем пользователей, фамилия которых совпадает с `Smith`constquery=User.findOne({'name.last','Smith'})// выбираем поля `name` и `occupation`query.select('name occupation')// выполняем запросquery.exec((err,user)=>{if(err)returnhandleError(err)console.log(user)})
В приведенном примере переменная query имеет тип Query. Query позволяет формировать запрос двумя способами
1 2 3 4 5 6 7 8 9101112131415161718192021222324
// Документ JSONUser.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 раза
Она определяется в 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
Значением валидатора является либо соответствующий примитив - условие, которому должно удовлетворять значение поля, либо массив, где первым элементом является примитив, а вторым - сообщение об ошибке
constbreakfastSchema=newSchema({eggs:{type:Number,min:[3,'Слишком мало яиц'],max:6,},bacon:{type:Number,required:[true,'Без бекона?'],},drink:{type:String,enum:['Кофе','Чай'],required:function(){returnthis.bacon>=2;},},});constBreakfast=model('Breakfast',breakfastSchema);constbadBreakfast=newBreakfast({eggs:2,bacon:2,drink:'Молоко',});leterror=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 requiredbadBreakfast.bacon=null;error=badBreakfast.validateSync();error.errors['bacon'].message;// Без бекона?
Обратите внимание, что настройка unique не является валидатором. Это утилита для генерации уникальных индексов
Кастомные валидаторы определяются следующим образом:
1 2 3 4 5 6 7 8 910111213141516171819202122232425
constuserSchema=newSchema({phone:{type:String,validate:{validator:(v)=>/+7\d{10}/.test(v),message:(props)=>`${props.value} не является валидным номером сотового телефона!`,},required:[true,'Номер телефона является обязательным',],},});constUser=model('User',userSchema);constuser=newUser();user.phone='3214256';leterror=user.validateSync();error.errors['phone'].message;// `3214256` не является валидным номером сотового телефона!user.phone='';error=user.validateSync();error.errors['phone'].message;// Номер телефона является обязательным
Если при выполнении валидации возникли ошибки, возвращается объект errors, каждое значение которого представляет собой объект ValidatorError (unique возвращает DuplicateKeyError). Объект ValidatorError содержит свойства kind, path, value и message. Он также может содержать свойство reason. Если в валидаторе было выброшено исключение, данное свойство будет содержать это исключение.
Перед запуском валидаторов Mongoose выполняет проверку значений на соответствие типам. Данный процесс называется кастингом (casting). При провале кастинга, объект error.errors будет содержать объект CastError. Кастинг выполняется перед валидацией, при провале кастинга валидация не выполняется.
Для того, чтобы сделать вложенный объект обязательным, следует определить его в виде схемы
1 2 3 4 5 6 7 8 9101112131415161718192021
// Будет выброшено исключение, поскольку `name` не является самостоятельным путемletuserSchema=newSchema({name:{first:String,last:String,},required:true,});constnameSchema=newSchema({first:String,last:String,});// так будет работатьuserSchema=newSchema({name:{type:nameSchema,required:true,},});
Mongoose также поддерживает валидацию для операций update(), updateOne(), updateMany() и findOneAndUpdate(). По умолчанию такая валидацию отключена. Для того, чтобы ее включить, следует установить настройку runValidators в значение true
Обратите внимание: валидаторы обновления имеют некоторые особенности, связанные с потерей контекста (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().
При возникновении ошибки в хуке, другие хуки или соответствующая функция не выполняются. Вместо этого, ошибка передается в колбек, а возвращенный промис отклоняется. Существует несколько способов обработки таких ошибок:
schema.pre('save',(next)=>{consterr=newError('Что-то пошло не так');// При вызове `next()` с аргументом, предполагается, что данный аргумент// является ошибкойnext(err);});schema.pre('save',()=>// Также можно вернуть отклоненный промисnewPromise((res,rej)=>{reject(newError('Что-то пошло не так'));}));schema.pre('save',()=>{// или выбросить синхронное исключение,thrownewError('Что-то пошло не так');});schema.pre('save',async()=>{awaitPromise.resolve();// или выбросить исключение в асинхронной функцииthrownewError('Что-то пошло не так');});// Позже// Изменения не будут сохранены в БД, поскольку в предварительном хуке возникает ошибкаdoc.save((err)=>{console.error(err);// Что-то пошло не так});
Последующие посредники выполняются после метода и всех его предварительных посредников
1 2 3 4 5 6 7 8 9101112131415161718
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(), предназначенная для вызова следующего посредника в цепочке
1 2 3 4 5 6 7 8 9101112131415
schema.post('save',(doc,next)=>{consttimerId=setTimeout(()=>{console.log('post1');// Запускаем второй хукnext();clearTimeout(timerId);},100);});// Не будет выполняться до вызова `next()` в первом посредникеschema.post('save',(doc,next)=>{console.log('post2');next();});
Функция save() запускает хуки validate(), поскольку Mongoose имеет встроенный хук pre('save'), вызывающий validate(). Это означает, что все хуки pre('validate') и post('validate') вызываются перед хуками pre('save').
Для remove() поддерживаются как хуки документов, так и хуки запросов.
Для переключения хука remove() между Document.remove() и Model.remove() следует передать объект с настройками в Schema.pre() или Schema.post()
1 2 3 4 5 6 7 8 9101112131415161718
// Только посредник документаschema.pre('remove',{document:true,query:false},()=>{console.log('Удаление документа');});// Только посредник запроса. Будет вызываться только для `Model.remove()`,// но не для `doc.remove()`schema.pre('remove',{query:true,document:false},()=>{console.log('Удаление');});
Выполнение посредника, обычно, останавливается при первом вызове next() с ошибкой. Тем не менее, существует специальный тип последующих посредников - посредники для обработки ошибок, которые выполняются при возникновении ошибок. Такие посредники могут использоваться для вывода сообщений об ошибках в удобочитаемом формате.
Посредники для обработки ошибок определяются как посредники, в качестве первого параметра принимающие возникшую ошибку
1 2 3 4 5 6 7 8 9101112131415161718192021222324
constchema=newSchema({name:{type:String,// При сохранении дубликата,// будет выброшена `MongoError` с кодом 11000unique:true,},});// Обработчик принимает три параметра: возникшую ошибку, документ// и функцию `next()`schema.post('save',(err,doc,next)=>{if(err.name==='MongoError'&&err.code===11000){next(newError('Попытка сохранения дубликата'));}else{next();}});// Это запустит обработчик ошибок `post('save')`User.create([{name:'John Smith'},{name:'John Smith'},]);
Рассматриваемые посредники также работают с посредниками запросов. Мы может определить хук post('update'), который будет перехватывать ошибки, связанные с дубликатами
1 2 3 4 5 6 7 8 9101112131415161718192021
schema.post('update',(err,res,next)=>{if(err.name==='MongoError'&&err.code===11000){next(newError('Попытка создания дубликата'));}else{next();}});constusers=[{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) - это процесс автоматической замены определенных путей в документе документами из других коллекций. Мы можем заполнять пути единичными документами, несколькими документами, единичным объектом, несколькими документами или всеми документами, возвращаемыми запросом. Рассмотрим пример
У нас имеется две модели. У модели User есть поле posts, значением которого является массив ObjectId. Настройка ref сообщает Mongoose, какую модель использовать в процессе популяции, в нашем случае такой моделью является Post. Все сохраняемые здесь _id должны быть _id документов из модели Post.
Обратите внимание: в качестве ссылок могут использоваться ObjectId, String, Number и Buffer. Однако, по возможности всегда следует использовать ObjectId.
Сохранение ссылок похоже на сохранение обычных свойств: достаточно присвоить значение _id
1 2 3 4 5 6 7 8 910111213141516171819
constauthor=newUser({_id:newmongoose.Types.ObjectId(),name:'John Smith',age:30,});author.save((err)=>{if(err)returnhandleError(err);constpost1=newPost({title:'Мой первый пост',author:author._id,// присваиваем `_id` из `author`});post1.save((err)=>{if(err)returnhandleError(err);// готово});});
Заполним поле author поста с помощью строителя запроса
1 2 3 4 5 6 7 8 91011
Post.findOne({title:'Мой первый пост'}).populate('author').exec((err,post)=>{if(err)returnhandleError(err);console.log('Автором поста является %s',post.author.name);// Автором поста является John Smith});
Это может использоваться для получения id автора. Однако, специально для таких случаев Mongoose предоставляет геттер _id, позволяющий получать идентификатор независимо от заполняемости поля
1234567
post.populated('author');// truthypost.author._id;// ObjectIdpost.depopulate('author');post.populated('author');// undefinedpost.author._id;// ObjectId, это возможно благодаря специальному геттеру
Для выполнения выборки достаточно передать методу populate() строку в качестве второго аргумента
1 2 3 4 5 6 7 8 91011
Post.findOne({title:'Мой первый пост'}).populate('author','name')// вернется только имя автора.exec((err,post)=>{if(err)returnhandleError(err);console.log('Автора зовут %s',post.author.name);// Автора зовут John Smithconsole.log('Автору %s лет',post.author.age);// Автору null лет});
Что если мы хотим заполнить массив подписчиков на основе их возраста и только их именами?
12345678
Post.find().populate({path:'subscribers',match:{age:{$gte:21}},// больше или равно, greater than or equal// явно исключаем `_id`select:'name -_id',}).exec();
При отсутствии совпадения, массив subscribers будет пустым.
1 2 3 4 5 6 7 8 910111213
constpost=awaitPost.findOne({title:'Мой первый пост',}).populate({path:'author',name:{// не равно, not equal$ne:'John Smith',},}).exec();post.author;// null
Мы можем обнаружить, что при использовании объекта author, нам недоступен список постов. Это объясняется тем, что объекты story не были помещены (pushed) в author.posts.
Если у нас имеется хорошая причина для сохранения массива дочерних указателей (child pointers), мы можем push() документы в массив
constdb1=mongoose.createConnection(MONGO_URI1);constdb2=mongoose.createConnection(MONGO_URI2);constconversationSchema=newSchema({messagesCount:Number,});constConversation=db2.model('Conversation',conversationSchema);consteventSchema=newSchema({name:String,conversation:{type:ObjectId,ref:Conversation,// `ref` - это класс модели, а не строка},});constEvent=db1.model('Event',eventSchema);constevents=awaitEvent.find().populate('conversations');// или когда мы не имеем доступа к `Conversation` при определении `eventSchema`constevents=awaitEvent.find().populate('conversations',(model:Conversation));
Mongoose также может осуществлять заполнение из нескольких коллекций на основе значения свойства документа. Предположим, что у нас имеется схема для комментариев. Пользователь может оставлять комментарии как к посту, так и к товару
1 2 3 4 5 6 7 8 9101112131415161718192021222324
constcommentSchema=newSchema({body:{type:String,required:true,},on:{type:Schema.Types.ObjectId,required:true,// Для определения правильной модели `Mongoose` будет использовать свойства `onModel`refPath:'onModel',},onModel:{type:String,required:true,enum:['Post','Product'],},});constProduct=model('Product',newSchema({name:String}));constPost=model('Post',newSchema({title:String}));constComment=model('Comment',commentSchema);
Другими словами, refPath позволяет определить, какую модель Mongoose должен использовать для каждого документа
1 2 3 4 5 6 7 8 910111213141516171819202122
constbook=awaitProduct.create({name:'Book1'});constpost=awaitPost.create({title:'Post1'});constcommentOnBook=awaitComment.create({body:'Comment1',on:book._id,onModel:'Product',});constcommentOnPost=awaitComment.create({body:'Comment2',on:post._id,onModel:'Post',});// `populate()` работает, несмотря на то, что один комментарий// ссылается на коллекцию `Product`, а другой - на коллекцию `Post`constcomments=awaitComment.find().populate('on').sort({body:1});comments[0].on.name;// Book1comments[1].on.title;// Post1
constpersonSchema=newSchema({name:String,band:String})constbandSchema=newSchema({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}// настройки запроса})constPerson=model('Person',personSchema)constBand=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
// Всегда выполняем заполнение при поискеMySchema.pre('find',function(){this.populate('user');});// Выполняем заполнение после поискаMySchema.post('find',async(docs)=>{for(constdocofdocs){if(doc.isPublish){awaitdoc.populate('user').execPopulate();}}});// Выполняем заполнение после сохраненияMySchema.post('save',(doc,next)=>{doc.populate('user').execPopulate().then(()=>{next();});});
Mongoose.connect() - метод для подключения к MongoDB
1 2 3 4 5 6 7 8 910111213141516171819202122
constmongoose=require('mongoose');constMONGO_URI=require('./config');constoptions={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');});
const{Schema}=require('mongoose');// автоматическое создание полей `createdAt` и `updatedAt`constoptions={timestamps:true,};constuserSchema=newSchema({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.method() - позволяет добавлять методы в схему
1 2 3 4 5 6 7 8 9101112131415161718
constuserSchema=newSchema({name:{first:String,last:String,},});userSchema.method('fullName',function(){return`${this.name.first}${this.name.last}`;});constUser=model('User',userSchema);constuser=newUser({name:{first:'John',last:'Smith',},});console.log(user.fullName());// John Smith
Schema.path() - метод для получения/установки путей (типов полей)
12
schema.path('name');// возвращается тип поля `name`schema.path('name',Number);// типом `name` теперь является `Number`
Schema.post() - метод для определения последующих хуков
Schema.pre() - метод для определения предварительных хуков
Schema.remove() - метод для удаления поля или полей в случае передачи массива
Schema.get(), Schema.set() - методы для получения и установки настроек схемы
Schema.static() - позволяет добавлять статические методы в схему
Model() - класс, который используется для взаимодействия с MongoDB. Экземпляры этого класса называются документами
1 2 3 4 5 6 7 8 9101112
const{Schema,model}=require('mongoose')constuserSchema=newSchema({...})constUser=model('User',userSchema)constnewUser=newUser({...})// сохраняем пользователя в БДawaitnewUser.save()// получаем пользователя из БДconstuser=awaitUser.findOne({...})
Model.bulkWrite() - позволяет отправить сразу несколько операций (переданных в виде массива) в MongoDB. Поддерживаемые операции:
insertOne
updateOne
updateMany
deleteOne
deleteMany
replaceOne
Model.estimatedDocumentCount() - возвращает количество документов, удовлетворяющих условию
Model.deleteMany() - удаляет из коллекции все документы, удовлетворяющие условию. Похож на remove(), но удаляет все документы независимо от настройки single
Model.deleteOne(conditions, options?, callback?) - удаляет из коллекции первый документ, удовлетворяющий условию
1
awaitUser.deleteOne({name:'John Smith'});
Model.events - диспетчер событий, который может использоваться для глобальной обработки ошибок
1
Model.events.on('error',(err)=>handleError(err));
Model.find() - метод для поиска документов
1 2 3 4 5 6 7 8 910111213141516
Model.find(filter,projection?,options?,callback?)// все документыawaitUser.find({})// всех пользователей с именем `John`, старше 18awaitUser.find({name:'John',age:{$gt:18}}).exec()// с результатами в колбекеawaitUser.find({name:'John',age:{$gt:18}},(err,docs)=>{})// регулярное выражение и выборка полейawaitUser.find({name:/john/i},'name friends').exec()// с настройкамиawaitUser.find({name:/john/i},null,{skip:10}).exec()
Model.findById() - метод для поиска документа по _id. findById(id) почти идентичен findOne({ _id: id })
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, объект создается при отсутствии
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, документ заменяется, а не обновляется
Model.init() - метод для генерации индексов при autoIndex: false
Model.insertMany(doc(s), options?, callback?) - метод для добавления единичного документа или массива документов в БД. Все добавляемые документы должны быть валидными (если не установлено ordered: false). Данный метод быстрее create(), поскольку отправляет в БД всего одну операцию (create() отправляет операцию для каждого документа)
Model.populate(doc(s), options, callback?) - метод для заполнения ссылок единичного документа или массива документов документами из других коллекций. options - либо строка с указанием пути, либо объект с настройками:
Model.remove(conditions, options?, callback?) - метод для удаления документов из коллекции по условию. Для удаления первого совпавшего документа следует установить single: true
12
constresult=awaitUser.remove({name:/john/i});result.deletedCount;// количество удаленных документов
Model.replaceOne(filter, doc, options?, callback?) - то же самое, что и update(), за исключением того, что существующий документ полностью заменяется переданным (не допускается атомарных операций типа $set)
Model.update(filter, doc, options?, callback?) - метод для обновления документа без его возвращения. Важные настройки:
Model.updateOne(filter, doc, options?, callback?) - то же самое, что update(), но без настроек multi и overwrite
Model.updateMany(filter, docs, options?, callback?) - то же самое, что update(), но обновляются все документы, удовлетворяющие условию, независимо от настройки multi
Model.watch(pipeline?, options?) - метод, позволяющий следить за изменениями соответствующей коллекции. Можно регистрировать следующие события:
Document.depopulate(path) - принимает заполняемое поле и возвращает его к незаполненному состоянию
1234567
Post.findOne().populate('author').exec((err,post)=>{console.log(post.author.name);// John Smithpost.depopulate('author');console.log(doc.author);// ObjectId});
Document.equals() - возвращает true, если документ равен другому документу. Документы сравниваются по _id. Если документы не имеют _id, используется функция deepEqual()
Document.execPopulate() - метод для явного выполнения популяции, возвращающий промис. Используется для интеграции промисов
Document.overwrite(obj) - перезаписывает значения документа значениями obj, кроме иммутабельных свойств. Похож на set(), но те свойства, которых нет в Document.obj, удаляются
Document.parent() - если документ является субдокументом, возвращается его предок
Document.populate(path?, callback?) - заполняет ссылки документа, выполняет колбек после завершения. Для того, чтобы получить промис, данный метод следует использовать совместно с execPopulate()
Query.select(arg) - определяет, какие поля документа должны включаться, а какие не должны. Флаг - означает исключение поля, флаг + - принудительное включение (например, для полей, исключенных на уровне схемы)
1234567
// включить поля `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 в запрос на обновление без изменения операции