Mongoose - это библиотека JavaScript, часто используемая в приложении Node.js с базой данных MongoDB. В данной статье я собираюсь познакомить вас с Mongoose и MongoDB и, что более важно, показать, где их уместно использовать в вашем приложении.
Что такое MongoDB?
Для начала рассмотрим MongoDB. MongoDB - это база данных, которая хранит ваши данные в виде документов. Как правило, эти документы имеют JSON (* JavaScript Object Notation - текстовый формат обмена данными, основанный на JavaScript. Здесь и далее примеч. пер.) - подобную структуру:
{ firstName: "Jamie", lastName: "Munro"}
Затем документ помещается внутрь коллекции. Например, в вышеуказанном примере документа определяется объект user
. Далее этот объект user
стал бы, скорее всего, частью коллекции под названием users
.
Одна из основных особенностей MongoDB - гибкость структуры её данных. Несмотря на то, что в первом примере объект user
имел свойства firstName
и lastName
, эти свойства могут отсутствовать в других документах user
коллекции users
. Именно это отличает MongoDB от баз данных SQL (* structured query language — язык структурированных запросов), например, MySQL или Microsoft SQL Server, в которых для каждого объекта, хранящегося в базе данных, необходима фиксированная схема.
За счет способности создавать динамические объекты, которые сохраняются в виде документов в базе данных, в игру вступает Mongoose.
Что такое Mongoose?
Mongoose - это ODM (* Object Document Mapper - объектно-документный отобразитель). Это означает, что Mongoose позволяет вам определять объекты со строго-типизированной схемой, соответствующей документу MongoDB.
Mongoose предоставляет огромный набор функциональных возможностей для создания и работы со схемами. На данный момент Mongoose содержит восемь SchemaTypes (* типы данных схемы), которые может иметь свойство, сохраняемое в MongoDB. Эти типы следующие:
- String
- Number
- Date
- Buffer
- Boolean
- Mixed
- ObjectId (* уникальный идентификатор объекта, первичный ключ, _id)
- Array
Для каждого типа данных можно:
- задать значение по умолчанию
- задать пользовательскую функцию проверки данных
- указать, что поле необходимо заполнить
- задать get-функцию (геттер), которая позволяет вам проводить манипуляции с данными до их возвращения в виде объекта
- задать set-функцию (* сеттер), которая позволяет вам проводить манипуляции с данными до их сохранения в базу данных
- определить индексы для более быстрого получения данных
Кроме этих общих возможностей для некоторых типов данных также можно настроить особенности сохранения и получения данных из базы данных. Например, для типа данных String
можно указать следующие дополнительные опции:
- конвертация данных в нижний регистр
- конвертация данных в верхний регистр
- обрезка данных перед сохранением
- определение регулярного выражения, которое позволяет в процессе проверки данных ограничить разрешенные для сохранения варианты данны
- определение перечня, который позволяет установить список допустимых строк
Для свойств типа Number
и Date
можно задать минимально и максимально допустимое значение.
Большинство из восьми допустимых типов данных должны быть вам хорошо знакомы. Однако, некоторые (Buffer
, Mixed
, ObjectId
и Array
) могут вызвать затруднения.
Тип данных Buffer позволяет вам сохранять двоичные данные. Типичным примером двоичных данных может послужить изображение или закодированный файл, например, документ в PDF-формате (* формат переносимого документа).
Тип данных Mixed
используется для превращения свойства в "неразборчивое" поле (поле, в котором допустимы данные любого типа). Подобно тому, как многие разработчики используют MongoDB для различных целей, в этом поле можно хранить данные различного типа, поскольку отсутствует определенная структура. С осторожностью используйте этот тип данных, поскольку он ограничивает возможности, предоставляемые Mongoose, например, проверку данных и отслеживание изменений сущности для автоматического обновления свойства при сохранении.
Тип данных ObjectId
используется обычно для определения ссылки на другой документ в вашей базе данных. Например, если бы у вас имелась коллекция книг и авторов, документ книги мог бы содержать свойство ObjectId
, ссылающееся на определенного автора документа.
Тип данных Array
позволяет вам сохранять JavaScript-подобные массивы. Благодаря этому типу данных вы можете выполнять над данными типичные JavaScript операции над массивами, например, push, pop, shift, slice и т.д.
Краткое повторение
Перед тем, как двинуться далее и писать код, мне хотелось бы подвести итог того, что мы только что выучили. MongoDB - это база данных, которая позволяет вам сохранять документы с динамической структурой. Эти документы сохраняются внутри коллекции.
Mongoose - это библиотека JavaScript, позволяющая вам определять схемы со строго-типизированными данными. Сразу после определения схемы Mongoose дает вам возможность создать Model (модель), основанную на определенной схеме. Затем модель синхронизируется с документом MongoDB с помощью определения схемы модели.
Сразу после определения схем и моделей вы можете пользоваться различными функциями Mongoose для проверки, сохранения, удаления и запроса ваших данных, используя обычные функции MongoDB. Мы еще рассмотрим это более подробно на конкретных примерах.
Установка MongoDB
До того, как начать создавать схемы и модели Mongoose, нам необходимо установить и настроить MongoDB. Я бы порекомендовал вам зайти на страницу загрузки MongoDB. Имеется несколько различных вариантов установки. Я выбрал Community Server. Данный вариант позволяет вам установить версию, предназначенную именно для вашей операционной системы. Также MongoDB предлагает вариант Enterprise Server и вариант облачной установки. Поскольку целые книги можно было бы написать об установке, настройке и мониторинге MongoDB, я остановился на варианте Community Server.
Как только вы загрузили и установили MongoDB для выбранной вами операционной системы, вам необходимо будет запустить базу данных. Вместо того, чтобы заново изобретать колесо, я хотел бы предложить вам почитать документацию MongoDB об установке MongoDB версии Community.
Я подожду вас, пока вы настроите MongoDB. Как только вы справились с вышесказанным, мы можем перейти к инсталляции Mongoose для соединения с вашей только что установленной базой данных MongoDB.
Установка Mongoose
Mongoose - это библиотека JavaScript. Я собираюсь использовать её в приложении Node.js. Если у вас уже установлен Node.js, то вы можете перейти к следующему разделу. Если же не установлен, я рекомендую вам начать с посещения страницы загрузки Node.js и выбора установщика для вашей операционной системы.
Как только Node.js установлен и настроен, я собираюсь создать новое приложение и затем установить npm (* диспетчер пакетов Node) модуль Mongoose.
После перехода в консоли в папку, куда бы вы хотели установить ваше приложение, вы можете выполнить следующие команды:
mkdir mongoose_basicscd mongoose_basicsnpm init
При инициализации моего приложения я оставил значения всех запрашиваемых параметров по умолчанию. Теперь я установлю модуль mongoose следующим образом:
npm install mongoose --save
После выполнения всех необходимых предварительных условий, давайте подключимся к базе данных MongoDB. Я разместил следующий код в файле index.js, поскольку я выбрал его как стартовую точку моего приложения:
var mongoose = require("mongoose");mongoose.connect("mongodb://localhost/mongoose_basics");
В первой строке кода мы подключаем библиотеку mongoose
. Далее я открываю соединение с базой данных, которую я назвал mongoose_basics
, используя функцию connect
.
Функция connect
принимает еще два других необязательных параметра. Второй параметр - объект опций, где вы можете указать, при необходимости, например, username (имя пользователя) и password (пароль). Третий параметр, который также может быть и вторым, если у вас не определены опции, - это функция обратного вызова, которая будет вызвана после попытки соединения с базой данных. Функцию обратного вызова можно использовать двумя способами:
mongoose.connect(uri, options, function(error) {// Check error in initial connection. There is no 2nd param to the callback.});// Or using promisesmongoose.connect(uri, options).then(() => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ },err => { /** handle initial connection error */ });
Чтобы избежать потенциальной необходимости введения в JavaScript Promises, я буду использовать первый способ. Ниже приводится обновленный index.js::
var mongoose = require("mongoose");mongoose.connect("mongodb://localhost/mongoose_basics", function (err) { if (err) throw err; console.log("Successfully connected");});
В случае ошибки при подключении к базе данных выбрасывается исключение, и все дальнейшее исполнение функции прерывается. При отсутствии ошибки в консоль выводится сообщение об успешном соединении.
Теперь Mongoose установлена и подключена к базе данных под названием mongoose_basics
. Мое соединение с MongoDB не использует ни username, ни password, ни пользовательского порта. Если вам необходимо указать эти опции или любую другую при подключении, я рекомендую вам просмотреть документацию Mongoose по подключению. В документации дается объяснение как многих доступных опций, так и процесса создания нескольких соединений, объединения соединений, реплик и т.д.
После удачного соединения давайте перейдем к определению схемы.
Определение Mongoose Schema (* схемы)
В начале статьи я показал вам объект user
, который имел два свойства: firstName
и lastName
. В следующем примере я переделал этот документ в схему:
var userSchema = mongoose.Schema({ firstName: String, lastName: String});
Это очень простая схема, которая содержит всего лишь два свойства без атрибутов, связанных с ней. Давайте распространим наш пример, сделав свойства first и last name дочерними объектами свойства name
. Свойство name
будет содержать свойства first и last name. Также я добавлю свойство created
типа Date
.
var userSchema = mongoose.Schema({ name: { firstName: String,lastName: String }, created: Date});
Как вы видите, Mongoose позволяет мне создавать очень гибкие схемы со множеством возможных комбинаций организации данных.
В следующем примере я собираюсь создать две новые схемы (author
и book
) и показать вам, как создать связь с другой схемой. Схема book
будет содержать ссылку на схему author
.
var authorSchema = mongoose.Schema({ _id: mongoose.Schema.Types.ObjectId, name: { firstName: String, lastName: String}, biography: String, twitter: String, facebook: String, linkedin: String, profilePicture: Buffer, created: { type: Date, default: Date.now }});
Выше приводится схема author
, которая распространяет схему user
, что я создал в предыдущем примере. Чтобы связать Author и Book, в схеме author
первым свойством указываем _id
типа ObjectId
. _id
- это стандартный синтаксис для обозначения первичного ключа в Mongoose и MongoDB. Далее, как и в схеме user
, я определил свойство name
, содержащее first и last name автора.
Распространяя схему user
, схема author
содержит несколько дополнительных свойств типа String
. Также я добавил свойство типа Buffer
, в котором можно было бы расположить изображение профиля автора. Последнее свойство содержит дату создания автора; однако, вы можете обратить внимание, что оно создано немного по-иному, так как в нем указано значение по умолчанию "сейчас". При сохранении автора в базу данных, данному свойству будет присвоено значение текущей даты/времени.
Чтобы завершить примеры схем, давайте создадим схему book
, которая содержит ссылку на автора, за счет использования свойства типа ObjectId
.
var bookSchema = mongoose.Schema({ _id: mongoose.Schema.Types.ObjectId, title: String, summary: String, isbn: String, thumbnail: Buffer, author: { type: mongoose.Schema.Types.ObjectId, ref: "Author" }, ratings: [ { summary: String, detail: String, numberOfStars: Number, created: { type: Date, default: Date.now } } ], created: { type: Date, default: Date.now }});
Схема book
содержит несколько свойств типа String
. Как было упомянуто ранее, эта схема содержит ссылку на схему author
. Схема book также
содержит свойство ratings
типа Array
, чтобы продемонстрировать вам возможности определения схем. Каждый элемент этого массива содержит свойства summary
, detail
, numberOfStars
и created
date.
Mongoose дает вам возможность создавать схемы со ссылками на другие схемы или, как в примере выше со свойством ratings
, позволяет создавать Array
дочерних свойств, который может содержаться в привязанной схеме (author в нашем примере) или же в текущей схеме, как в примере выше (схема book со свойством ratings типа Array
).
Создание и сохранение Mongoose Models (* моделей)
Поскольку на примере схем author
и book
мы увидели гибкость схемы Mongoose, я собираюсь продолжить использовать их и создать на их основе модели Author
и Book
.
var Author = mongoose.model("Author", authorSchema);var Book = mongoose.model("Book", bookSchema);
После сохранения модели в MongoDB создается Document (* документ) с теми же свойствами, что определены в схеме, на основе которой была создана модель.
Чтобы продемонстрировать создание и сохранение объекта, в следующем примере я собираюсь создать несколько объектов: одну модель Author
и несколько моделей Book
. Сразу после создания эти объекты будут сохранены в MongoDB при помощи метода модели save
.
var jamieAuthor = new Author { _id: new mongoose.Types.ObjectId(), name: { firstName: "Jamie", lastName: "Munro" }, biography: "Jamie is the author of ASP.NET MVC 5 with Bootstrap and Knockout.js.", twitter: "https://twitter.com/endyourif", facebook: "https://www.facebook.com/End-Your-If-194251957252562/"};jamieAuthor.save(function(err) {if (err) throw err;console.log("Author successfully saved.");var mvcBook = new Book { _id: new mongoose.Types.ObjectId(), title: "ASP.NET MVC 5 with Bootstrap and Knockout.js", author: jamieAuthor._id, ratings:[{ summary: "Great read" }]};mvcBook.save(function(err) {if (err) throw err;console.log("Book successfully saved.");});var knockoutBook = new Book { _id: new mongoose.Types.ObjectId(), title: "Knockout.js: Building Dynamic Client-Side Web Applications", author: jamieAuthor._id};knockoutBook.save(function(err) {if (err) throw err;console.log("Book successfully saved.");});});
В примере выше я самым бессовестным образом разместил ссылки на две мои новые книги. В начале примера мы создаем и сохраняем jamieObject
, созданный при помощи модели Author
. В случае ошибки внутри функции save
объекта jamieObject
приложение выбросит исключение. В случае же отсутствия ошибки внутри функции save
будут созданы и сохранены два объекта book. Подобно объекту jamieObject
, в этих объектах в случае возникновения ошибки при сохранении выбрасывается исключение. В ином случае в консоль выводится сообщение об успешном сохранении.
Для создания ссылки на Author, оба объекта book ссылаются на первичный ключ _id
схемы author
в свойстве author
схемы book
.
Проверка данных перед сохранением
Общепринято наполнение данных для создания модели в форме на веб-странице. По этой причине, хорошо бы проверить эти данные перед сохранением модели в MongoDB.
В следующем примере я обновил предыдущую схему author, добавив проверку данных следующих свойств: firstName
, twitter
, facebook
и linkedin
.
var authorSchema = mongoose.Schema({ _id: mongoose.Schema.Types.ObjectId, name: {firstName: {type: String,required: true},lastName: String},biography: String,twitter: {type: String,validate: {validator: function(text) {return text.indexOf("https://twitter.com/") === 0;},message: "Twitter handle must start with https://twitter.com/"}},facebook: {type: String,validate: {validator: function(text) {return text.indexOf("https://www.facebook.com/") === 0;},message: "Facebook must start with https://www.facebook.com/"}},linkedin: {type: String,validate: {validator: function(text) {return text.indexOf("https://www.linkedin.com/") === 0;},message: "LinkedIn must start with https://www.linkedin.com/"}},profilePicture: Buffer,created: { type: Date,default: Date.now}});
Для свойства firstName
был задан атрибут required
. Теперь при вызове функции save
, Mongoose вернет ошибку с сообщением о необходимости указания значения свойства firstName
. Я решил сделать свойство lastName
без необходимости указания его значения на случай, если авторами в моей базе данных были бы Cher или Madonna (* отсутствует фамилия).
Для свойств twitter
, facebook
и linkedin
используются подобные пользовательские валидаторы. Они проверяются на соответствие начала их значений соответствующему доменному имени социальных сетей. Поскольку это необязательные для заполнения поля, валидатор применяется только в случае поступления данных для этого свойства.
Поиск и обновление данных
Введение в Mongoose не было бы завершенным без примера поиска записи и обновления одного или более свойств этого объекта.
Mongoose предоставляет несколько различных функций для поиска данных определенной модели. Эти функции следующие: find
, findOne
и findById
.
Функции find
и findOne
получают в качестве аргумента объект, позволяющий осуществлять сложные запросы. Функция же findById
получает только одно значение функции обратного вызова (скоро будет пример). В следующем примере я продемонстрирую вам, как можно сделать выборку книг, содержащих в своем названии строку "mvc".
Book.find({title: /mvc/i}).exec(function(err, books) {if (err) throw err;console.log(books);});
Внутри функции find
я осуществляю поиск нечувствительной к регистру строки "mvc" по свойству title
. Это осуществляется с помощью того же синтаксиса, что используется для поиска строки в JavaScript.
Функцию find таккже можно "прицепить" к другим методам запроса, например, where
, and
, or
, limit
, sort
, any
и т.д.
Давайте распространим наш предыдущий пример, ограничив количество результатов до пяти первых книг и отсортировав их по дате создания по убыванию. Результатом будут первые пять наиболее новых книг, содержащих в названии строку "mvc".
Book.find({title: /mvc/i}).sort("-created").limit(5).exec(function(err, books) {if (err) throw err;console.log(books);});
После применения функции find
порядок последующих функций не имеет значения, поскольку из всех сцепленных функций формируется единый запрос и функции не выполняются до вызова функции exec
.
Как я упоминал ранее, функция findById
выполняется немного по-другому. Она выполняется сразу же и принимает в качестве одного из аргументов функцию обратного вызова, и не позволяет сцепливание функций. В следующем примере я запрашиваю необходимого автора по его _id
.
Author.findById("59b31406beefa1082819e72f", function(err, author) { if (err) throw err; console.log(author);});
У вас значение _id
может быть немного другим. Я скопировал значение _id
из предыдущего console.log
, когда осуществляли поиск книг, содержащих в названии строку "mvc".
Сразу после возвращения объекта вы можете изменить любое из его свойств и обновить его. Как только вы внесли необходимые изменения, вы вызываете метод save
также, как вы делали и при создании объекта. В следующем примере я распространю пример с функцией findbyId
и обновлю свойство linkedin
автора.
Author.findById("59b31406beefa1082819e72f", function(err, author) {if (err) throw err;author.linkedin = "https://www.linkedin.com/in/jamie-munro-8064ba1a/";author.save(function(err) {if (err) throw err;console.log("Author updated successfully");});});
После успешного получения автора устанавливается значение свойства linkedin
и вызывается функция save
. Mongoose способна заметить изменение свойства linkedin
и передать состояние, обновленное только по модифицированным свойствам, в MongoDB. В случае возникновения ошибки при сохранении будет выброшено исключение и приложение прекратит работу. При отсутствии ошибок в консоль будет выведено сообщение об успешном изменении.
Также Mongoose предоставляет возможность найти объект и сразу обновить его при помощи функций с соответствующими названиями: findByIdAndUpdate
и findOneAndUpdate
. Давайте обновим предыдущий пример, чтобы показать функцию findByIdAndUpdate
в действии.
Author.findByIdAndUpdate("59b31406beefa1082819e72f", { linkedin: "https://www.linkedin.com/in/jamie-munro-8064ba1a/" }, function(err, author) { if (err) throw err; console.log(author);});
В предыдущем примере свойства, что мы хотим обновить, передаются в функцию findByIdAndUpdate
как объект вторым параметром. При этом функция обратного вызова является третьим параметром. После удачного обновления возвращенный объект author
содержит обновленную информацию. Он выводиться в консоль, чтобы мы увидели обновленные свойства автора.
Полный код примера
По ходу статьи мы рассматривали кусочки кода, описывающие работу отдельных действий, например, создание схемы, создание модели и т.д. Давайте теперь воссоединим все воедино в одном полном примере.
Для начала я создал два дополнительных файла: author.js
и book.js
. Данные файлы содержат соответствующие оределения схем и создание моделей. Последняя строка кода делает модель доступной для использования в файле index.js
.
Давайте начнем с файла author.js:
var mongoose = require("mongoose");var authorSchema = mongoose.Schema({ _id: mongoose.Schema.Types.ObjectId, name: {firstName: {type: String,required: true},lastName: String},biography: String,twitter: {type: String,validate: {validator: function(text) {return text.indexOf("https://twitter.com/") === 0;},message: "Twitter handle must start with https://twitter.com/"}},facebook: {type: String,validate: {validator: function(text) {return text.indexOf("https://www.facebook.com/") === 0;},message: "Facebook must start with https://www.facebook.com/"}},linkedin: {type: String,validate: {validator: function(text) {return text.indexOf("https://www.linkedin.com/") === 0;},message: "LinkedIn must start with https://www.linkedin.com/"}},profilePicture: Buffer,created: { type: Date,default: Date.now}});var Author = mongoose.model("Author", authorSchema);module.exports = Author;
Далее переходим к файлу book.js
:
var mongoose = require("mongoose");var bookSchema = mongoose.Schema({ _id: mongoose.Schema.Types.ObjectId, title: String,summary: String,isbn: String,thumbnail: Buffer,author: { type: mongoose.Schema.Types.ObjectId, ref: "Author" },ratings: [{summary: String,detail: String,numberOfStars: Number,created: { type: Date,default: Date.now}}],created: { type: Date,default: Date.now}});var Book = mongoose.model("Book", bookSchema);module.exports = Book;
И, наконец, обновленнй файл index.js
:
var mongoose = require("mongoose");var Author = require("./author");var Book = require("./book");mongoose.connect("mongodb://localhost/mongoose_basics", function (err) { if (err) throw err;console.log("Successfully connected");var jamieAuthor = new Author({_id: new mongoose.Types.ObjectId(),name: {firstName: "Jamie",lastName: "Munro"},biography: "Jamie is the author of ASP.NET MVC 5 with Bootstrap and Knockout.js.",twitter: "https://twitter.com/endyourif",facebook: "https://www.facebook.com/End-Your-If-194251957252562/"});jamieAuthor.save(function(err) {if (err) throw err;console.log("Author successfully saved.");var mvcBook = new Book({_id: new mongoose.Types.ObjectId(),title: "ASP.NET MVC 5 with Bootstrap and Knockout.js",author: jamieAuthor._id,ratings:[{summary: "Great read"}]});mvcBook.save(function(err) {if (err) throw err;console.log("Book successfully saved.");});var knockoutBook = new Book({_id: new mongoose.Types.ObjectId(),title: "Knockout.js: Building Dynamic Client-Side Web Applications",author: jamieAuthor._id});knockoutBook.save(function(err) {if (err) throw err;console.log("Book successfully saved.");});});});
В вышеуказанном примере все действия Mongoose содержатся внутри функции connect
. Файлы author
и book
подключаются при помощи функции require
после подключения mongoose
.
Если MongoDB запущена, вы теперь можете запустить полное приложение Node.js при помощи следующей команды:
node index.js
После сохранения некоторых данных в базу я обновил файл index.js
, добавив функции поиска, следующим образом:
var mongoose = require("mongoose");var Author = require("./author");var Book = require("./book");mongoose.connect("mongodb://localhost/mongoose_basics", function (err) { if (err) throw err;console.log("Successfully connected");Book.find({title: /mvc/i}).sort("-created").limit(5).exec(function(err, books) {if (err) throw err;console.log(books);});Author.findById("59b31406beefa1082819e72f", function(err, author) {if (err) throw err;author.linkedin = "https://www.linkedin.com/in/jamie-munro-8064ba1a/";author.save(function(err) {if (err) throw err;console.log("Author updated successfully");});});Author.findByIdAndUpdate("59b31406beefa1082819e72f", { linkedin: "https://www.linkedin.com/in/jamie-munro-8064ba1a/" }, function(err, author) {if (err) throw err;console.log(author);});});
Опять-таки, вы можете запустить приложение при помощи следующей команды: node index.js
.
Резюме
После прочтения данной статьи вы должны быть в состоянии создавать чрезвычайно гибкие схемы и модели Mongoose, осуществлять простую или сложную проверку данных, создавать и обновлять документы и, наконец, осуществлять поиск созданных документов.
Надеюсь, теперь вы чувствуете себя уверенным пользователем Mongoose. Если вы хотите узнать больше о Mongoose, я бы рекомендовал вам изучить Mongoose Guides, в котором объясняются более продвинутые темы, например, population, middleware, promises и т.д.
Удачной охоты (да простят меня мангусты)!