First be sure you have MongoDB and Node.js installed.
Next install Mongoose from the command line using npm
:
npm install mongoose --save
Now say we like fuzzy kittens and want to record every kitten we ever meet
in MongoDB.
The first thing we need to do is include mongoose in our project and open a
connection to the test
database on our locally running instance of MongoDB.
// getting-started.js
const mongoose = require('mongoose');
main().catch(err => console.log(err));
async function main() {
await mongoose.connect('mongodb://127.0.0.1:27017/test');
// use `await mongoose.connect('mongodb://user:password@127.0.0.1:27017/test');` if your database has auth enabled
}
For brevity, let’s assume that all following code is within the main()
function.
With Mongoose, everything is derived from a Schema.
Let’s get a reference to it and define our kittens.
const kittySchema = new mongoose.Schema({
name: String
});
So far so good. We’ve got a schema with one property, name
, which will be a String
. The next step is compiling our schema into a Model.
const Kitten = mongoose.model('Kitten', kittySchema);
A model is a class with which we construct documents.
In this case, each document will be a kitten with properties and behaviors as declared in our schema.
Let’s create a kitten document representing the little guy we just met on the sidewalk outside:
const silence = new Kitten({ name: 'Silence' });
console.log(silence.name); // 'Silence'
Kittens can meow, so let’s take a look at how to add «speak» functionality
to our documents:
// NOTE: methods must be added to the schema before compiling it with mongoose.model()
kittySchema.methods.speak = function speak() {
const greeting = this.name
? 'Meow name is ' + this.name
: 'I don\'t have a name';
console.log(greeting);
};
const Kitten = mongoose.model('Kitten', kittySchema);
Functions added to the methods
property of a schema get compiled into
the Model
prototype and exposed on each document instance:
const fluffy = new Kitten({ name: 'fluffy' });
fluffy.speak(); // "Meow name is fluffy"
We have talking kittens! But we still haven’t saved anything to MongoDB.
Each document can be saved to the database by calling its save method. The first argument to the callback will be an error if any occurred.
await fluffy.save();
fluffy.speak();
Say time goes by and we want to display all the kittens we’ve seen.
We can access all of the kitten documents through our Kitten model.
const kittens = await Kitten.find();
console.log(kittens);
We just logged all of the kittens in our db to the console.
If we want to filter our kittens by name, Mongoose supports MongoDBs rich querying syntax.
await Kitten.find({ name: /^fluff/ });
This performs a search for all documents with a name property that begins
with «fluff» and returns the result as an array of kittens to the callback.
Congratulations
That’s the end of our quick start. We created a schema, added a custom document method, saved and queried kittens in MongoDB using Mongoose. Head over to the guide, or API docs for more.
In this article, we’ll learn how Mongoose, a third-party library for MongoDB, can help you to structure and access your data with ease.
What is Mongoose?
Mongoose is an ODM (Object Data Modeling) library for MongoDB. While you don’t need to use an Object Data Modeling (ODM) or Object Relational Mapping (ORM) tool to have a great experience with MongoDB, some developers prefer them. Many Node.js developers choose to work with Mongoose to help with data modeling, schema enforcement, model validation, and general data manipulation. And Mongoose makes these tasks effortless.
If you want to hear from the maintainer of Mongoose, Val Karpov, give this episode of the MongoDB Podcast a listen!
Why Mongoose?
By default, MongoDB has a flexible data model. This makes MongoDB databases very easy to alter and update in the future. But a lot of developers are accustomed to having rigid schemas.
Mongoose forces a semi-rigid schema from the beginning. With Mongoose, developers must define a Schema and Model.
What is a schema?
A schema defines the structure of your collection documents. A Mongoose schema maps directly to a MongoDB collection.
With schemas, we define each field and its data type. Permitted types are:
What is a model?
Models take your schema and apply it to each document in its collection.
Models are responsible for all document interactions like creating, reading, updating, and deleting (CRUD).
An important note: the first argument passed to the model should be the singular form of your collection name. Mongoose automatically changes this to the plural form, transforms it to lowercase, and uses that for the database collection name.
In this example, Blog
translates to the blogs
collection.
Environment setup
Let’s set up our environment. I’m going to assume you have Node.js installed already.
We’ll run the following commands from the terminal to get going:
This will create the project directory, initialize, install the packages we need, and open the project in VS Code.
Let’s add a script to our package.json
file to run our project. We will also use ES Modules instead of Common JS, so we’ll add the module type
as well. This will also allow us to use top-level await
.
Connecting to MongoDB
Now we’ll create the index.js
file and use Mongoose to connect to MongoDB.
You could connect to a local MongoDB instance, but for this article we are going to use a free MongoDB Atlas cluster. If you don’t already have an account, it’s easy to sign up for a free MongoDB Atlas cluster here.
After creating your cluster, you should replace the connection string above with your connection string including your username and password.
The connection string that you copy from the MongoDB Atlas dashboard will reference the myFirstDatabase
database. Change that to whatever you would like to call your database.
Creating a schema and model
Before we do anything with our connection, we’ll need to create a schema and model.
Ideally, you would create a schema/model file for each schema that is needed. So we’ll create a new folder/file structure: model/Blog.js
.
Inserting data // method 1
Now that we have our first model and schema set up, we can start inserting data into our database.
Back in the index.js
file, let’s insert a new blog article.
We first need to import the Blog
model that we created. Next, we create a new blog object and then use the save()
method to insert it into our MongoDB database.
Let’s add a bit more after that to log what is currently in the database. We’ll use the findOne()
method for this.
You should see the document inserted logged in your terminal.
Because we are using nodemon
in this project, every time you save a file, the code will run again. If you want to insert a bunch of articles, just keep saving. 😄
Inserting data // method 2
In the previous example, we used the save()
Mongoose method to insert the document into our database. This requires two actions: instantiating the object, and then saving it.
Alternatively, we can do this in one action using the Mongoose create()
method.
This method is much better! Not only can we insert our document, but we also get returned the document along with its _id
when we console log it.
Update data
Mongoose makes updating data very convenient too. Expanding on the previous example, let’s change the title
of our article.
We can directly edit the local object, and then use the save()
method to write the update back to the database. I don’t think it can get much easier than that!
Finding data
Let’s make sure we are updating the correct document. We’ll use a special Mongoose method, findById()
, to get our document by its ObjectId.
Notice that we use the exec()
Mongoose function. This is technically optional and returns a promise. In my experience, it’s better to use this function since it will prevent some head-scratching issues. If you want to read more about it, check out this note in the Mongoose docs about promises.
Projecting document fields
Just like with the standard MongoDB Node.js driver, we can project only the fields that we need. Let’s only get the title
, slug
, and content
fields.
The second parameter can be of type Object|String|Array<String>
to specify which fields we would like to project. In this case, we used a String
.
Deleting data
Just like in the standard MongoDB Node.js driver, we have the deleteOne()
and deleteMany()
methods.
Validation
Notice that the documents we have inserted so far have not contained an author
, dates, or comments
. So far, we have defined what the structure of our document should look like, but we have not defined which fields are actually required. At this point any field can be omitted.
Let’s set some required fields in our Blog.js
schema.
When including validation on a field, we pass an object as its value.
value: String
is the same as value: {type: String}
.
There are several validation methods that can be used.
We can set required
to true on any fields we would like to be required.
For the slug
, we want the string to always be in lowercase. For this, we can set lowercase
to true. This will take the slug input and convert it to lowercase before saving the document to the database.
For our created
date, we can set the default buy using an arrow function. We also want this date to be impossible to change later. We can do that by setting immutable
to true.
Validators only run on the create or save methods.
Other useful methods
Mongoose uses many standard MongoDB methods plus introduces many extra helper methods that are abstracted from regular MongoDB methods. Next, we’ll go over just a few of them.
The exists()
method returns either null
or the ObjectId of a document that matches the provided query.
Mongoose also has its own style of querying data. The where()
method allows us to chain and build queries.
Either of these methods work. Use whichever seems more natural to you.
You can also chain multiple where()
methods to include even the most complicated query.
To include projection when using the where()
method, chain the select()
method after your query.
Multiple schemas
It’s important to understand your options when modeling data.
If you’re coming from a relational database background, you’ll be used to having separate tables for all of your related data.
Generally, in MongoDB, data that is accessed together should be stored together.
You should plan this out ahead of time if possible. Nest data within the same schema when it makes sense.
If you have the need for separate schemas, Mongoose makes it a breeze.
Let’s create another schema so that we can see how multiple schemas can be used together.
We’ll create a new file, User.js
, in the model folder.
For the email
, we are using a new property, minLength
, to require a minimum character length for this string.
Now we’ll reference this new user model in our blog schema for the author
and comments.user
.
Here, we set the author
and comments.user
to SchemaTypes.ObjectId
and added a ref
, or reference, to the user model.
This will allow us to “join” our data a bit later.
And don’t forget to destructure SchemaTypes
from mongoose
at the top of the file.
Lastly, let’s update the index.js
file. We’ll need to import our new user model, create a new user, and create a new article with the new user’s _id
.
Notice now that there is a users
collection along with the blogs
collection in the MongoDB database.
You’ll now see only the user _id
in the author field. So, how do we get all of the info for the author along with the article?
We can use the populate()
Mongoose method.
Now the data for the author
is populated, or “joined,” into the article
data. Mongoose actually uses the MongoDB $lookup
method behind the scenes.
Middleware
In Mongoose, middleware are functions that run before and/or during the execution of asynchronous functions at the schema level.
Here’s an example. Let’s update the updated
date every time an article is saved or updated. We’ll add this to our Blog.js
model.
Then in the index.js
file, we’ll find an article, update the title, and then save it.
Notice that we now have an updated
date!
Besides pre()
, there is also a post()
mongoose middleware function.
Next steps
I think our example here could use another schema for the comments
. Try creating that schema and testing it by adding a few users and comments.
There are many other great Mongoose helper methods that are not covered here. Be sure to check out the official documentation for references and more examples.
Conclusion
I think it’s great that developers have many options for connecting and manipulating data in MongoDB. Whether you prefer Mongoose or the standard MongoDB drivers, in the end, it’s all about the data and what’s best for your application and use case.
I can see why Mongoose appeals to many developers and I think I’ll use it more in the future.
Mongoose — это библиотека JavaScript, часто используемая в приложении Node.js с базой данных MongoDB. В данной статье я собираюсь познакомить вас с Mongoose и MongoDB и, что более важно, показать, где их уместно использовать в вашем приложении.
Что такое MongoDB?
Для начала рассмотрим MongoDB. MongoDB — это база данных, которая хранит ваши данные в виде документов. Как правило, эти документы имеют JSON (* JavaScript Object Notation — текстовый формат обмена данными, основанный на JavaScript. Здесь и далее примеч. пер.) — подобную структуру:
1 |
{
|
2 |
firstName: "Jamie", |
3 |
lastName: "Munro" |
4 |
}
|
Затем документ помещается внутрь коллекции. Например, в вышеуказанном примере документа определяется объект 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.
После перехода в консоли в папку, куда бы вы хотели установить ваше приложение, вы можете выполнить следующие команды:
1 |
mkdir mongoose_basics
|
2 |
cd mongoose_basics
|
3 |
npm init |
При инициализации моего приложения я оставил значения всех запрашиваемых параметров по умолчанию. Теперь я установлю модуль mongoose следующим образом:
1 |
npm install mongoose --save |
После выполнения всех необходимых предварительных условий, давайте подключимся к базе данных MongoDB. Я разместил следующий код в файле index.js, поскольку я выбрал его как стартовую точку моего приложения:
1 |
var mongoose = require('mongoose'); |
2 |
|
3 |
mongoose.connect('mongodb://localhost/mongoose_basics'); |
В первой строке кода мы подключаем библиотеку mongoose
. Далее я открываю соединение с базой данных, которую я назвал mongoose_basics
, используя функцию connect
.
Функция connect
принимает еще два других необязательных параметра. Второй параметр — объект опций, где вы можете указать, при необходимости, например, username (имя пользователя) и password (пароль). Третий параметр, который также может быть и вторым, если у вас не определены опции, — это функция обратного вызова, которая будет вызвана после попытки соединения с базой данных. Функцию обратного вызова можно использовать двумя способами:
1 |
mongoose.connect(uri, options, function(error) { |
2 |
|
3 |
// Check error in initial connection. There is no 2nd param to the callback.
|
4 |
|
5 |
});
|
6 |
|
7 |
// Or using promises
|
8 |
|
9 |
mongoose.connect(uri, options).then( |
10 |
|
11 |
() => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ }, |
12 |
|
13 |
err => { /** handle initial connection error */ } |
14 |
|
15 |
);
|
Чтобы избежать потенциальной необходимости введения в JavaScript Promises, я буду использовать первый способ. Ниже приводится обновленный index.js::
1 |
var mongoose = require('mongoose'); |
2 |
|
3 |
mongoose.connect('mongodb://localhost/mongoose_basics', function (err) { |
4 |
|
5 |
if (err) throw err; |
6 |
|
7 |
console.log('Successfully connected'); |
8 |
|
9 |
});
|
В случае ошибки при подключении к базе данных выбрасывается исключение, и все дальнейшее исполнение функции прерывается. При отсутствии ошибки в консоль выводится сообщение об успешном соединении.
Теперь Mongoose установлена и подключена к базе данных под названием mongoose_basics
. Мое соединение с MongoDB не использует ни username, ни password, ни пользовательского порта. Если вам необходимо указать эти опции или любую другую при подключении, я рекомендую вам просмотреть документацию Mongoose по подключению. В документации дается объяснение как многих доступных опций, так и процесса создания нескольких соединений, объединения соединений, реплик и т.д.
После удачного соединения давайте перейдем к определению схемы.
Определение Mongoose Schema (* схемы)
В начале статьи я показал вам объект user
, который имел два свойства: firstName
и lastName
. В следующем примере я переделал этот документ в схему:
1 |
var userSchema = mongoose.Schema({ |
2 |
firstName: String, |
3 |
lastName: String |
4 |
});
|
Это очень простая схема, которая содержит всего лишь два свойства без атрибутов, связанных с ней. Давайте распространим наш пример, сделав свойства first и last name дочерними объектами свойства name
. Свойство name
будет содержать свойства first и last name. Также я добавлю свойство created
типа Date
.
1 |
var userSchema = mongoose.Schema({ |
2 |
name: { |
3 |
firstName: String, |
4 |
lastName: String |
5 |
},
|
6 |
created: Date |
7 |
});
|
Как вы видите, Mongoose позволяет мне создавать очень гибкие схемы со множеством возможных комбинаций организации данных.
В следующем примере я собираюсь создать две новые схемы (author
и book
) и показать вам, как создать связь с другой схемой. Схема book
будет содержать ссылку на схему author
.
1 |
var authorSchema = mongoose.Schema({ |
2 |
_id: mongoose.Schema.Types.ObjectId, |
3 |
name: { |
4 |
firstName: String, |
5 |
lastName: String |
6 |
},
|
7 |
biography: String, |
8 |
twitter: String, |
9 |
facebook: String, |
10 |
linkedin: String, |
11 |
profilePicture: Buffer, |
12 |
created: { |
13 |
type: Date, |
14 |
default: Date.now |
15 |
}
|
16 |
});
|
Выше приводится схема author
, которая распространяет схему user
, что я создал в предыдущем примере. Чтобы связать Author и Book, в схеме author
первым свойством указываем _id
типа ObjectId
. _id
— это стандартный синтаксис для обозначения первичного ключа в Mongoose и MongoDB. Далее, как и в схеме user
, я определил свойство name
, содержащее first и last name автора.
Распространяя схему user
, схема author
содержит несколько дополнительных свойств типа String
. Также я добавил свойство типа Buffer
, в котором можно было бы расположить изображение профиля автора. Последнее свойство содержит дату создания автора; однако, вы можете обратить внимание, что оно создано немного по-иному, так как в нем указано значение по умолчанию «сейчас». При сохранении автора в базу данных, данному свойству будет присвоено значение текущей даты/времени.
Чтобы завершить примеры схем, давайте создадим схему book
, которая содержит ссылку на автора, за счет использования свойства типа ObjectId
.
1 |
var bookSchema = mongoose.Schema({ |
2 |
_id: mongoose.Schema.Types.ObjectId, |
3 |
title: String, |
4 |
summary: String, |
5 |
isbn: String, |
6 |
thumbnail: Buffer, |
7 |
author: { |
8 |
type: mongoose.Schema.Types.ObjectId, |
9 |
ref: 'Author' |
10 |
},
|
11 |
ratings: [ |
12 |
{
|
13 |
summary: String, |
14 |
detail: String, |
15 |
numberOfStars: Number, |
16 |
created: { |
17 |
type: Date, |
18 |
default: Date.now |
19 |
}
|
20 |
}
|
21 |
],
|
22 |
created: { |
23 |
type: Date, |
24 |
default: Date.now |
25 |
}
|
26 |
});
|
Схема 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
.
1 |
var Author = mongoose.model('Author', authorSchema); |
2 |
|
3 |
var Book = mongoose.model('Book', bookSchema); |
После сохранения модели в MongoDB создается Document (* документ) с теми же свойствами, что определены в схеме, на основе которой была создана модель.
Чтобы продемонстрировать создание и сохранение объекта, в следующем примере я собираюсь создать несколько объектов: одну модель Author
и несколько моделей Book
. Сразу после создания эти объекты будут сохранены в MongoDB при помощи метода модели save
.
1 |
var jamieAuthor = new Author { |
2 |
_id: new mongoose.Types.ObjectId(), |
3 |
name: { |
4 |
firstName: 'Jamie', |
5 |
lastName: 'Munro' |
6 |
},
|
7 |
biography: 'Jamie is the author of ASP.NET MVC 5 with Bootstrap and Knockout.js.', |
8 |
twitter: 'https://twitter.com/endyourif', |
9 |
facebook: 'https://www.facebook.com/End-Your-If-194251957252562/' |
10 |
};
|
11 |
|
12 |
jamieAuthor.save(function(err) { |
13 |
if (err) throw err; |
14 |
|
15 |
console.log('Author successfully saved.'); |
16 |
|
17 |
var mvcBook = new Book { |
18 |
_id: new mongoose.Types.ObjectId(), |
19 |
title: 'ASP.NET MVC 5 with Bootstrap and Knockout.js', |
20 |
author: jamieAuthor._id, |
21 |
ratings:[{ |
22 |
summary: 'Great read' |
23 |
}]
|
24 |
};
|
25 |
|
26 |
mvcBook.save(function(err) { |
27 |
if (err) throw err; |
28 |
|
29 |
console.log('Book successfully saved.'); |
30 |
});
|
31 |
|
32 |
var knockoutBook = new Book { |
33 |
_id: new mongoose.Types.ObjectId(), |
34 |
title: 'Knockout.js: Building Dynamic Client-Side Web Applications', |
35 |
author: jamieAuthor._id |
36 |
};
|
37 |
|
38 |
knockoutBook.save(function(err) { |
39 |
if (err) throw err; |
40 |
|
41 |
console.log('Book successfully saved.'); |
42 |
});
|
43 |
});
|
В примере выше я самым бессовестным образом разместил ссылки на две мои новые книги. В начале примера мы создаем и сохраняем jamieObject
, созданный при помощи модели Author
. В случае ошибки внутри функции save
объекта jamieObject
приложение выбросит исключение. В случае же отсутствия ошибки внутри функции save
будут созданы и сохранены два объекта book. Подобно объекту jamieObject
, в этих объектах в случае возникновения ошибки при сохранении выбрасывается исключение. В ином случае в консоль выводится сообщение об успешном сохранении.
Для создания ссылки на Author, оба объекта book ссылаются на первичный ключ _id
схемы author
в свойстве author
схемы book
.
Проверка данных перед сохранением
Общепринято наполнение данных для создания модели в форме на веб-странице. По этой причине, хорошо бы проверить эти данные перед сохранением модели в MongoDB.
В следующем примере я обновил предыдущую схему author, добавив проверку данных следующих свойств: firstName
, twitter
, facebook
и linkedin
.
1 |
var authorSchema = mongoose.Schema({ |
2 |
_id: mongoose.Schema.Types.ObjectId, |
3 |
name: { |
4 |
firstName: { |
5 |
type: String, |
6 |
required: true |
7 |
},
|
8 |
lastName: String |
9 |
},
|
10 |
biography: String, |
11 |
twitter: { |
12 |
type: String, |
13 |
validate: { |
14 |
validator: function(text) { |
15 |
return text.indexOf('https://twitter.com/') === 0; |
16 |
},
|
17 |
message: 'Twitter handle must start with https://twitter.com/' |
18 |
}
|
19 |
},
|
20 |
facebook: { |
21 |
type: String, |
22 |
validate: { |
23 |
validator: function(text) { |
24 |
return text.indexOf('https://www.facebook.com/') === 0; |
25 |
},
|
26 |
message: 'Facebook must start with https://www.facebook.com/' |
27 |
}
|
28 |
},
|
29 |
linkedin: { |
30 |
type: String, |
31 |
validate: { |
32 |
validator: function(text) { |
33 |
return text.indexOf('https://www.linkedin.com/') === 0; |
34 |
},
|
35 |
message: 'LinkedIn must start with https://www.linkedin.com/' |
36 |
}
|
37 |
},
|
38 |
profilePicture: Buffer, |
39 |
created: { |
40 |
type: Date, |
41 |
default: Date.now |
42 |
}
|
43 |
});
|
Для свойства firstName
был задан атрибут required
. Теперь при вызове функции save
, Mongoose вернет ошибку с сообщением о необходимости указания значения свойства firstName
. Я решил сделать свойство lastName
без необходимости указания его значения на случай, если авторами в моей базе данных были бы Cher или Madonna (* отсутствует фамилия).
Для свойств twitter
, facebook
и linkedin
используются подобные пользовательские валидаторы. Они проверяются на соответствие начала их значений соответствующему доменному имени социальных сетей. Поскольку это необязательные для заполнения поля, валидатор применяется только в случае поступления данных для этого свойства.
Поиск и обновление данных
Введение в Mongoose не было бы завершенным без примера поиска записи и обновления одного или более свойств этого объекта.
Mongoose предоставляет несколько различных функций для поиска данных определенной модели. Эти функции следующие: find
, findOne
и findById
.
Функции find
и findOne
получают в качестве аргумента объект, позволяющий осуществлять сложные запросы. Функция же findById
получает только одно значение функции обратного вызова (скоро будет пример). В следующем примере я продемонстрирую вам, как можно сделать выборку книг, содержащих в своем названии строку ‘mvc’.
1 |
Book.find({ |
2 |
title: /mvc/i |
3 |
}).exec(function(err, books) { |
4 |
if (err) throw err; |
5 |
|
6 |
console.log(books); |
7 |
});
|
Внутри функции find
я осуществляю поиск нечувствительной к регистру строки ‘mvc’ по свойству title
. Это осуществляется с помощью того же синтаксиса, что используется для поиска строки в JavaScript.
Функцию find таккже можно «прицепить» к другим методам запроса, например, where
, and
, or
, limit
, sort
, any
и т.д.
Давайте распространим наш предыдущий пример, ограничив количество результатов до пяти первых книг и отсортировав их по дате создания по убыванию. Результатом будут первые пять наиболее новых книг, содержащих в названии строку ‘mvc’.
1 |
Book.find({ |
2 |
title: /mvc/i |
3 |
}).sort('-created') |
4 |
.limit(5) |
5 |
.exec(function(err, books) { |
6 |
if (err) throw err; |
7 |
|
8 |
console.log(books); |
9 |
});
|
После применения функции find
порядок последующих функций не имеет значения, поскольку из всех сцепленных функций формируется единый запрос и функции не выполняются до вызова функции exec
.
Как я упоминал ранее, функция findById
выполняется немного по-другому. Она выполняется сразу же и принимает в качестве одного из аргументов функцию обратного вызова, и не позволяет сцепливание функций. В следующем примере я запрашиваю необходимого автора по его _id
.
1 |
Author.findById('59b31406beefa1082819e72f', function(err, author) { |
2 |
if (err) throw err; |
3 |
|
4 |
console.log(author); |
5 |
});
|
У вас значение _id
может быть немного другим. Я скопировал значение _id
из предыдущего console.log
, когда осуществляли поиск книг, содержащих в названии строку ‘mvc’.
Сразу после возвращения объекта вы можете изменить любое из его свойств и обновить его. Как только вы внесли необходимые изменения, вы вызываете метод save
также, как вы делали и при создании объекта. В следующем примере я распространю пример с функцией findbyId
и обновлю свойство linkedin
автора.
1 |
Author.findById('59b31406beefa1082819e72f', function(err, author) { |
2 |
if (err) throw err; |
3 |
|
4 |
author.linkedin = 'https://www.linkedin.com/in/jamie-munro-8064ba1a/'; |
5 |
|
6 |
author.save(function(err) { |
7 |
if (err) throw err; |
8 |
|
9 |
console.log('Author updated successfully'); |
10 |
});
|
11 |
});
|
После успешного получения автора устанавливается значение свойства linkedin
и вызывается функция save
. Mongoose способна заметить изменение свойства linkedin
и передать состояние, обновленное только по модифицированным свойствам, в MongoDB. В случае возникновения ошибки при сохранении будет выброшено исключение и приложение прекратит работу. При отсутствии ошибок в консоль будет выведено сообщение об успешном изменении.
Также Mongoose предоставляет возможность найти объект и сразу обновить его при помощи функций с соответствующими названиями: findByIdAndUpdate
и findOneAndUpdate
. Давайте обновим предыдущий пример, чтобы показать функцию findByIdAndUpdate
в действии.
1 |
Author.findByIdAndUpdate('59b31406beefa1082819e72f', |
2 |
{ linkedin: 'https://www.linkedin.com/in/jamie-munro-8064ba1a/' }, |
3 |
function(err, author) { |
4 |
if (err) throw err; |
5 |
|
6 |
console.log(author); |
7 |
});
|
В предыдущем примере свойства, что мы хотим обновить, передаются в функцию findByIdAndUpdate
как объект вторым параметром. При этом функция обратного вызова является третьим параметром. После удачного обновления возвращенный объект author
содержит обновленную информацию. Он выводиться в консоль, чтобы мы увидели обновленные свойства автора.
Полный код примера
По ходу статьи мы рассматривали кусочки кода, описывающие работу отдельных действий, например, создание схемы, создание модели и т.д. Давайте теперь воссоединим все воедино в одном полном примере.
Для начала я создал два дополнительных файла: author.js
и book.js
. Данные файлы содержат соответствующие оределения схем и создание моделей. Последняя строка кода делает модель доступной для использования в файле index.js
.
Давайте начнем с файла author.js:
1 |
var mongoose = require('mongoose'); |
2 |
|
3 |
var authorSchema = mongoose.Schema({ |
4 |
_id: mongoose.Schema.Types.ObjectId, |
5 |
name: { |
6 |
firstName: { |
7 |
type: String, |
8 |
required: true |
9 |
},
|
10 |
lastName: String |
11 |
},
|
12 |
biography: String, |
13 |
twitter: { |
14 |
type: String, |
15 |
validate: { |
16 |
validator: function(text) { |
17 |
return text.indexOf('https://twitter.com/') === 0; |
18 |
},
|
19 |
message: 'Twitter handle must start with https://twitter.com/' |
20 |
}
|
21 |
},
|
22 |
facebook: { |
23 |
type: String, |
24 |
validate: { |
25 |
validator: function(text) { |
26 |
return text.indexOf('https://www.facebook.com/') === 0; |
27 |
},
|
28 |
message: 'Facebook must start with https://www.facebook.com/' |
29 |
}
|
30 |
},
|
31 |
linkedin: { |
32 |
type: String, |
33 |
validate: { |
34 |
validator: function(text) { |
35 |
return text.indexOf('https://www.linkedin.com/') === 0; |
36 |
},
|
37 |
message: 'LinkedIn must start with https://www.linkedin.com/' |
38 |
}
|
39 |
},
|
40 |
profilePicture: Buffer, |
41 |
created: { |
42 |
type: Date, |
43 |
default: Date.now |
44 |
}
|
45 |
});
|
46 |
|
47 |
var Author = mongoose.model('Author', authorSchema); |
48 |
|
49 |
module.exports = Author; |
Далее переходим к файлу book.js
:
1 |
var mongoose = require('mongoose'); |
2 |
|
3 |
var bookSchema = mongoose.Schema({ |
4 |
_id: mongoose.Schema.Types.ObjectId, |
5 |
title: String, |
6 |
summary: String, |
7 |
isbn: String, |
8 |
thumbnail: Buffer, |
9 |
author: { |
10 |
type: mongoose.Schema.Types.ObjectId, |
11 |
ref: 'Author' |
12 |
},
|
13 |
ratings: [ |
14 |
{
|
15 |
summary: String, |
16 |
detail: String, |
17 |
numberOfStars: Number, |
18 |
created: { |
19 |
type: Date, |
20 |
default: Date.now |
21 |
}
|
22 |
}
|
23 |
],
|
24 |
created: { |
25 |
type: Date, |
26 |
default: Date.now |
27 |
}
|
28 |
});
|
29 |
|
30 |
var Book = mongoose.model('Book', bookSchema); |
31 |
|
32 |
module.exports = Book; |
И, наконец, обновленнй файл index.js
:
1 |
var mongoose = require('mongoose'); |
2 |
|
3 |
var Author = require('./author'); |
4 |
var Book = require('./book'); |
5 |
|
6 |
mongoose.connect('mongodb://localhost/mongoose_basics', function (err) { |
7 |
if (err) throw err; |
8 |
|
9 |
console.log('Successfully connected'); |
10 |
|
11 |
var jamieAuthor = new Author({ |
12 |
_id: new mongoose.Types.ObjectId(), |
13 |
name: { |
14 |
firstName: 'Jamie', |
15 |
lastName: 'Munro' |
16 |
},
|
17 |
biography: 'Jamie is the author of ASP.NET MVC 5 with Bootstrap and Knockout.js.', |
18 |
twitter: 'https://twitter.com/endyourif', |
19 |
facebook: 'https://www.facebook.com/End-Your-If-194251957252562/' |
20 |
});
|
21 |
|
22 |
jamieAuthor.save(function(err) { |
23 |
if (err) throw err; |
24 |
|
25 |
console.log('Author successfully saved.'); |
26 |
|
27 |
var mvcBook = new Book({ |
28 |
_id: new mongoose.Types.ObjectId(), |
29 |
title: 'ASP.NET MVC 5 with Bootstrap and Knockout.js', |
30 |
author: jamieAuthor._id, |
31 |
ratings:[{ |
32 |
summary: 'Great read' |
33 |
}]
|
34 |
});
|
35 |
|
36 |
mvcBook.save(function(err) { |
37 |
if (err) throw err; |
38 |
|
39 |
console.log('Book successfully saved.'); |
40 |
});
|
41 |
|
42 |
var knockoutBook = new Book({ |
43 |
_id: new mongoose.Types.ObjectId(), |
44 |
title: 'Knockout.js: Building Dynamic Client-Side Web Applications', |
45 |
author: jamieAuthor._id |
46 |
});
|
47 |
|
48 |
knockoutBook.save(function(err) { |
49 |
if (err) throw err; |
50 |
|
51 |
console.log('Book successfully saved.'); |
52 |
});
|
53 |
});
|
54 |
});
|
В вышеуказанном примере все действия Mongoose содержатся внутри функции connect
. Файлы author
и book
подключаются при помощи функции require
после подключения mongoose
.
Если MongoDB запущена, вы теперь можете запустить полное приложение Node.js при помощи следующей команды:
После сохранения некоторых данных в базу я обновил файл index.js
, добавив функции поиска, следующим образом:
1 |
var mongoose = require('mongoose'); |
2 |
|
3 |
var Author = require('./author'); |
4 |
var Book = require('./book'); |
5 |
|
6 |
mongoose.connect('mongodb://localhost/mongoose_basics', function (err) { |
7 |
if (err) throw err; |
8 |
|
9 |
console.log('Successfully connected'); |
10 |
|
11 |
Book.find({ |
12 |
title: /mvc/i |
13 |
}).sort('-created') |
14 |
.limit(5) |
15 |
.exec(function(err, books) { |
16 |
if (err) throw err; |
17 |
|
18 |
console.log(books); |
19 |
});
|
20 |
|
21 |
Author.findById('59b31406beefa1082819e72f', function(err, author) { |
22 |
if (err) throw err; |
23 |
|
24 |
author.linkedin = 'https://www.linkedin.com/in/jamie-munro-8064ba1a/'; |
25 |
|
26 |
author.save(function(err) { |
27 |
if (err) throw err; |
28 |
|
29 |
console.log('Author updated successfully'); |
30 |
});
|
31 |
});
|
32 |
|
33 |
Author.findByIdAndUpdate('59b31406beefa1082819e72f', { linkedin: 'https://www.linkedin.com/in/jamie-munro-8064ba1a/' }, function(err, author) { |
34 |
if (err) throw err; |
35 |
|
36 |
console.log(author); |
37 |
});
|
38 |
});
|
Опять-таки, вы можете запустить приложение при помощи следующей команды: node index.js
.
Резюме
После прочтения данной статьи вы должны быть в состоянии создавать чрезвычайно гибкие схемы и модели Mongoose, осуществлять простую или сложную проверку данных, создавать и обновлять документы и, наконец, осуществлять поиск созданных документов.
Надеюсь, теперь вы чувствуете себя уверенным пользователем Mongoose. Если вы хотите узнать больше о Mongoose, я бы рекомендовал вам изучить Mongoose Guides, в котором объясняются более продвинутые темы, например, population, middleware, promises и т.д.
Удачной охоты (да простят меня мангусты)!
Создано: 08-01-2020
Введение в MongoDB и Mongoose
MongoDB—база данных, которая хранит данные в виде документов для использования приложением. Как правило, документы имеют структуру, подобную JSON (JavaScript Object Notation—текстовый формат обмена данными, основанный на JavaScript). Mongo—нереляционная база данных «NoSQL». Это означает, что Mongo хранит все связанные данные в одной записи, а не хранит их во многих заранее заданных таблицах, как в базе данных SQL. Некоторые преимущества этой модели хранения заключаются в следующем:
- Масштабируемость: по умолчанию нереляционные базы данных распределяются (или «совместно используются») на множество систем, а не только на одну. Это облегчает повышение производительности при меньших затратах.
- Гибкость: новые наборы данных и свойств могут быть добавлены в документ без необходимости создавать новую таблицу для этих данных.
- Репликация: копии базы данных выполняются параллельно, поэтому, если одна из них не работает, одна из копий становится новым основным источником данных.
Хотя существует много нереляционных баз данных, использование Mongo с JSON в качестве структуры хранения документов делает его логичным выбором при изучении бэкенда JavaScript. Доступ к документам и их свойствам подобен доступу к объектам в JavaScript.
Mongoose.js—модуль npm для Node.js, который позволяет вам писать объекты для Mongo так же, как и в JavaScript. Это может облегчить создание документов для хранения в Mongo.
Работа над задачами в этом руководстве потребует написания кода на Glitch.
Запустите этот проект на Glitch по этой ссылке или клонируйте этот репозиторий на GitHub!
Размещение бесплатного экземпляра mongodb для проектов в MongoDB Atlas
Для решения задач в этом руководстве нужно будет сохранять кой-какие данные, для этого будет использоваться база данных MongoDB.
Чтобы создавать веб-приложения с помощью базы данных MongoDB можно использовать три пути:
- Для создания базы данных MongoDB и разработки приложения использовать собственный компьютер. Для этого вы должны установить сервер Node и сервер базы данных MongoDB на своем ПК.
- Для создания базы данных MongoDB использовать облачный сервис MongoDB Atlas, а приложение разрабатывать и запускать на локальном ПК. Этот способ будет рассмотрен в данной статье.
- Для создания базы данных MongoDB использовать облачный сервис MongoDB Atlas, а приложение разрабатывать и запускать на каком-нибудь облачном сервисе, например Glitch.
Чтобы не заморачиваться с установкой и настройкой MongoDB воспользуемся облачным сервисом MongoDB Atlas, который не только упростит конфигурацию базы данных, но и позволит иметь к этой базе доступ откуда угодно и в любое время. Руководство по настройке аккаунта в MongoDB Atlas и подключению экземпляра базы данных MongoDB читайте на этой странице.
Установка и настройка Mongoose и MongoDB
Дальнейшие действия предполагают, что у вас нет своего проекта, и что вы начнете с нуля.
В терминале создайте каталог myapp
и сделайте его рабочим.
md myapp
cd myapp
С помощью команды npm init
создайте файл package.json
.
npm init
Эта команда выдает целый ряд приглашений, например, приглашение указать имя и версию вашего приложения. На данный момент, достаточно просто нажать клавишу ВВОД, чтобы принять предлагаемые значения по умолчанию для большинства пунктов, кроме следующего:
entry point: (index.js)
Введите app.js или любое другое имя главного файла по своему желанию. Если вас устраивает index.js, нажмите клавишу ВВОД, чтобы принять предложенное имя файла по умолчанию.
Чтобы ваше приложение могло работать с базой данных MongoDB нужно установить драйвер. Установите драйвер MongoDB и его зависимости, выполнив в терминале из каталога myapp
следующую команду.
npm install mongodb
Теперь установите модуль mongoose в каталоге myapp
, набрав следующую команду в терминале.
npm install mongoose
После установки в каталоге myapp
будут находится два файла package.json
, package-lock.json
и каталог node_modules
. В файле package.json
будут добавлены зависимости:
"dependencies": {
"mongodb": "^3.4.1",
"mongoose": "^5.8.7"
}
Переменные окружения в файле .env
Для хранения переменных окружения вы будете использовать файл .env
. Создайте его в корне проекта и скопируйте в него URI базы данных MongoDB Atlas, полученный раннее:
MONGO_URI='mongodb+srv://<user>:<password>@cluster0-hsvns.mongodb.net/test?retryWrites=true&w=majority'
Обратите внимание: URI окружен одинарными (можно двойными) кавычками; между переменной MONGO_URI и знаком =
, а также, между знаком =
и URI не должно быть пробела; замените <user> на имя пользователя, а <password> на свой пароль в MongoDB Atlas. Там не должно быть символов <> (если только они не находятся в вашем пароле).
Обратите внимание, в файле .env
хранится пароль, поэтому при сохраненинии проекта в репозиторий, данный файл нужно включить в список исключений в файле .gitignore
.
Для того, чтобы переменные окружения из файла env
можно было использовать в приложении нужно установить пакет dotenv
:
npm install dotenv
В файле package.json
будет добавлена зависимость:
"dependencies": {
"dotenv": "^8.2.0",
}
Теперь, если вам необходимо использовать какую-нибудь переменную окружения из файла env
в одном из файлов вашего приложения, вы должны в этом файле просто подключить пакет dotenv
следующим образом:
require('dotenv').config();
Теперь все переменные из файла .env
будут доступны в process.env
. Чтобы прочитать значение переменной, например, PASSWORD нужно обратиться к свойству process.env.PASSWORD
.
Подключение БД MongoDB
В корне проекта создайте файл index.js
, в который скопируйте следующий код.
//Подключение к файлу модуля mongoose под именем mongoose
var mongoose = require('mongoose');
//Использование пакета dotenv для чтения переменных из файла .env в Node
require('dotenv').config();
//Соединение с базой данных
mongoose.connect(
process.env.MONGO_URI,
{ useNewUrlParser: true, useUnifiedTopology: true },
function (err) {
//Если при соединении с БД происходит ошибка, то выбрасывается исключение, и все дальнейшее исполнение функции прерывается.
if (err) throw err;
//Если соединение с БД выполнено успешно выводится сообщение 'БД подключена'
console.log('БД подключена');
}
);
В функции connect()
первый параметр process.env.MONGO_URI
— это URI для подключения приложения к БД (в данном случае значение свойства MONGO_URI хранится в файле .env
). Вторым параметром в функции connect()
является необязательный объект опций. Третий параметр — это функция обратного вызова, которая будет вызвана после попытки соединения с базой данных.
Создание модели
CRUD Часть I — создание
CRUD — это сокращение для операций Create, Read, Update and Delete (создать, прочесть, обновить и удалить). Эти операции являются основными для работы с базами данных, таких как MongoDB.
В mongoose все завязано на 2х ключевых понятиях Схема(Schema) – описание сущности и Модель – сама сущность.
Прежде всего вам нужна [схема]https://mongoosejs.com/docs/guide.html.
Создадайте схему и модель из неё.
В файл index.js
скопируйте следующий код.
require('dotenv').config();
const mongoose = require('mongoose');
mongoose.connect(
process.env.MONGO_URI,
{ useNewUrlParser: true, useUnifiedTopology: true },
function (err) {
if (err) throw err;
console.log('БД подключена');
}
);
//Создание схемы
var userSchema = new mongoose.Schema({
name: { type: String, default: 'Анонимный' },
age: { type: Number, min: 18, index: true },
});
//Создание модели из схемы.
const UserModel = mongoose.model('UserModel', userSchema);
Каждое поле в mongoose.Schema
характеризуется типом и может иметь дополнительные характеристики: default
, min
и max
(для Number), match
и enum
(для String), index
и unique
(для индексов). Подробнее о типах можно почитать тут.
В функции mongoose.model
первый параметр — это имя модели, второй параметр — имя схемы, из которой создается модель.
Схемы — это строительный блок для моделей. Модель позволяет создавать экземпляры ваших объектов, называемых документами.
Создание и сохранение записи модели
В файле index.js
замените содержимое на следующий код.
require('dotenv').config();
const mongoose = require('mongoose');
mongoose.connect(
process.env.MONGO_URI,
{ useNewUrlParser: true, useUnifiedTopology: true },
function (err) {
if (err) throw err;
console.log('БД подключена');
}
);
var userSchema = new mongoose.Schema({
name: { type: String, default: 'Анонимный' },
age: { type: Number, min: 18, index: true },
favoriteFoods: [String],
});
const UserModel = mongoose.model('UserModel', userSchema);
//Создание объекта модели, т. е. документа
var ivanPetrov = new UserModel({
name: 'Ivan Petrov',
age: 25,
favoriteFoods: ['чипсы', 'кока-кола'],
});
//Сохранение документа в БД
ivanPetrov.save(function (err, data) {
if (err) return console.error(err);
console.log('Пользователь с именем ' + data.name + ' сохранен');
});
Метод save()
должен сохранить документ в базе данных mongodb. Если сохранение прошло успешно, будет выведено на консоль ‘Пользователь с именем Ivan Petrov сохранен’, если же произошла ошибка, то будет выведено соответствующее сообщение об ошибке.
В вашей базе данных теперь должен быть один документ с именем «Ivan Petrov».
Создание нескольких записей с помощью model.create()
Выше было показано, как сохранить документ в базе данных mongodb с помощью метода mongoose save()
. Но что если нужно сохранить много документов, например, из массива. Для этого можно применить другой метод mongoose — create()
.
В файле index.js
замените содержимое на следующий код.
require('dotenv').config();
const mongoose = require('mongoose');
mongoose.connect(
process.env.MONGO_URI,
{ useNewUrlParser: true, useUnifiedTopology: true },
function (err) {
if (err) throw err;
console.log('БД подключена');
}
);
var userSchema = new mongoose.Schema({
name: { type: String, default: 'Анонимный' },
age: { type: Number, min: 18, index: true },
favoriteFoods: [String],
});
const UserModel = mongoose.model('UserModel', userSchema);
//Массив, из которого данные будут помещены в БД
var arrayUsers = [
{ name: 'Светлана', age: 21, favoriteFoods: ['чипсы', 'кофе'] },
{ name: 'Kamila', age: 35, favoriteFoods: ['гамбургер', 'кока-кола'] },
{ name: 'Олег', age: 27, favoriteFoods: ['роллы', 'кофе'] },
];
UserModel.create(arrayUsers, function (err, users) {
if (err) return console.log(err);
console.log('В базе данных созданы ' + users.length + ' документа');
});
Таким образом с помощью функции create()
из массива arrayUsers
были добавлены еще три документа в БД, а на консоль выведена сообщение «В базе данных созданы 3 документа». Обратите внимание, в базе данных теперь четыре документа.
Первый аргумент в методе Model.create()
— это документы в виде массива или объекта, которые будут вставлены в БД. Второй аргумент — это функция обратного вызова.
В функции обратного вызова в первый аргумент err
передается ошибка, а во второй аргумент users
передаётся массив arrayUsers
.
Использование model.find() для поиска в базе данных
В файл index.js
скопируйте следующий код.
require('dotenv').config();
const mongoose = require('mongoose');
mongoose.connect(
process.env.MONGO_URI,
{ useNewUrlParser: true, useUnifiedTopology: true },
function (err) {
if (err) throw err;
console.log('БД подключена');
}
);
var userSchema = new mongoose.Schema({
name: { type: String, default: 'Анонимный' },
age: { type: Number, min: 18, index: true },
});
const UserModel = mongoose.model('UserModel', userSchema);
var userName = 'Светлана';
//Поиск в БД
UserModel.find({ name: userName }, function (err, data) {
if (err) return console.log(err);
console.log(
'Все пользователи с именем ' +
userName +
' найдены. Их всего ' +
data.length
);
});
Первый параметр в функции find()
— это селектор, являющийся объектом, который указывает, что нужно искать в базе данных. Если селектор не указан, возвращаются все документы из БД. Вторым параметром в функции find()
является функция обратного вызова.
Функция find()
находит и возвращает все документы, соответствующие селектору. Результатом будет массив документов.
Если в результате будет слишком много документов, чтобы поместиться в памяти, используйте функцию cursor()
Использование model.findOne() для возвращения одного документа из базы данных
В mongoose есть метод findOne()
, который ведет себя как метод find()
, но возвращает только один документ (не массив). Даже если документов с данным параметром поиска несколько метод findOne()
возвращает первый найденный документ. Это особенно полезно при поиске по свойствам, которые вы объявили уникальными.
В файл index.js
скопируйте следующий код.
require('dotenv').config();
const mongoose = require('mongoose');
mongoose.connect(
process.env.MONGO_URI,
{ useNewUrlParser: true, useUnifiedTopology: true },
function (err) {
if (err) throw err;
console.log('БД подключена');
}
);
var userSchema = new mongoose.Schema({
name: { type: String, default: 'Анонимный' },
age: { type: Number, min: 18, index: true },
favoriteFoods: [String],
});
const UserModel = mongoose.model('UserModel', userSchema);
UserModel.findOne({ name: 'Светлана' }, function (err, data) {
if (err) return console.log(err);
console.log('Пользователь ' + data.name + ' найден');
});
Метод findOne()
находит в базе данных первый попавшийся документ со свойством { name: "Светлана" }
и возвращает его. Если в качестве первого параметра в функции findOne()
ничего не указано, mongoose вернет произвольный документ.
Использование model.findById() для поиска в базе данных по id
Когда в базу данных сохраняется документ, mongodb автоматически добавляет поле _id
и присваивает ему уникальный буквенно-цифровой ключ. Поиск по _id
является очень частой операцией, поэтому mongoose предоставляет специальный метод для этого — findById()
.
В файл index.js
скопируйте следующий код.
require('dotenv').config();
const mongoose = require('mongoose');
mongoose.connect(
process.env.MONGO_URI,
{ useNewUrlParser: true, useUnifiedTopology: true },
function (err) {
if (err) throw err;
console.log('БД подключена');
}
);
var userSchema = new mongoose.Schema({
name: { type: String, default: 'Анонимный' },
age: { type: Number, min: 18, index: true },
favoriteFoods: [String],
});
const UserModel = mongoose.model('UserModel', userSchema);
//Определенине id для поиска
var userId = '5e24c27a0d07d02119c39ed7';
//Поиск документа по _id
UserModel.findById(userId, function (err, data) {
if (err) return console.log(err);
console.log(
'Пользователь c id = ' +
data._id +
' найден, его зовут ' +
data.name +
', ему ' +
data.age +
' лет'
);
});
Если документ с указанным id найден, то на консоль будет выведено сообщение «Пользователь c id = 5e24c27a0d07d02119c39ed7 найден, его зовут Олег, ему 27 лет».
Обновление документов в БД с помощью стандартного поиска, присвоения и сохранения
Для того, чтобы изменить (обновить) документ в базе данных, в mongoose существуют методы update
, findByIdAndUpdate
и findOneAndUpdate
. Но сначала нелишнем будет узнать о классическом способе изменения документов. Этот способ состоит из уже изученных вами методов, а именно: findOne
, findById
и save
.
В файл index.js
скопируйте следующий код.
require('dotenv').config();
const mongoose = require('mongoose');
mongoose.connect(
process.env.MONGO_URI,
{ useNewUrlParser: true, useUnifiedTopology: true },
function (err) {
if (err) throw err;
console.log('БД подключена');
}
);
var userSchema = new mongoose.Schema({
name: { type: String, default: 'Анонимный' },
age: { type: Number, min: 18, index: true },
favoriteFoods: [String],
});
const UserModel = mongoose.model('UserModel', userSchema);
/*Обновление документа*/
//Поиск документа по _id
UserModel.findById('5e25a8e88170fb0f8ce90f6f', function (err, user) {
if (err) return console.error(err);
//Присвоение измененных значений
user.name = 'Светлана Иванова';
user.favoriteFoods.push('гамбургер');
//Сохранение документа в БД
user.save(function (err) {
if (err) throw err;
console.log('Информация о пользователе ' + user.name + ' обновлена');
});
});
Обновление документов в БД с помощью model.findOneAndUpdate()
В последних версиях mongoose есть методы, упрощающие обновление документов. Но некоторые более продвинутые функции (например, хуки pre/post, валидация) ведут себя по-другому при этом подходе, поэтому классический метод все еще полезен во многих ситуациях.
В файл index.js
скопируйте следующий код.
require('dotenv').config();
const mongoose = require('mongoose');
mongoose.connect(
process.env.MONGO_URI,
{ useNewUrlParser: true, useUnifiedTopology: true },
function (err) {
if (err) throw err;
console.log('БД подключена');
}
);
var userSchema = new mongoose.Schema({
name: { type: String, default: 'Анонимный' },
age: { type: Number, min: 18, index: true },
favoriteFoods: [String],
});
const UserModel = mongoose.model('UserModel', userSchema);
//Обновление документа в БД
UserModel.findOneAndUpdate(
{ name: 'Олег' },
{ name: 'Олег Сидоров', age: 28 },
{ new: true },
function (err, user) {
if (err) return console.error(err);
console.log('Информация о пользователе ' + user.name + ' обновлена');
}
);
Функция findOneAndUpdate()
находит пользователя по условию, указанному в первом параметре { name: 'Олег' }
, затем устанавливает свойства, указанные во втором параметре { name: 'Олег Сидоров', age: 28 }
. Третий параметр { new: true }
в функции findOneAndUpdate()
указывает на то, чтобы функция возвращала измененный документ, а не оригинал. Т. е. при при new
установленном в true
на консоле будет выведено 'Информация о пользователе Олег Сидоров обновлена'
, а при new
установленном в false
на консоле будет выведено 'Информация о пользователе Олег обновлена'
. По умолчанию new
установлено в false
. Четвертый параметр в функции findOneAndUpdate()
— это функция обратного вызова.
Удаление документов из MongoDB с помощью Mongoose
Для того, чтобы удалить документы из БД MongoDB в Mongoose существуют методы
remove()
, deleteMany()
, deleteOne()
, findOneAndDelete()
, findByIdAndRemove()
и findOneAndRemove()
.
Удаление одного документа с помощью model.findByIdAndRemove
В файл index.js
скопируйте следующий код.
require('dotenv').config();
const mongoose = require('mongoose');
mongoose.connect(
process.env.MONGO_URI,
{ useNewUrlParser: true, useUnifiedTopology: true },
function (err) {
if (err) throw err;
console.log('БД подключена');
}
);
var userSchema = new mongoose.Schema({
name: { type: String, default: 'Анонимный' },
age: { type: Number, min: 18, index: true },
favoriteFoods: [String],
});
const UserModel = mongoose.model('UserModel', userSchema);
UserModel.findByIdAndRemove('5e25a8e88170fb0f8ce90f71', function (err, user) {
if (err) return console.error(err);
console.log('Пользователь ' + user.name + ' удален из БД');
});
Метод findByIdAndRemove()
находит документ по Id
, заданному в первом параметре, и удаляяет этот документ. Если документ найден, то он возвращается в функцию обратного вызова (в данном случае, в параметр user
). Первый параметр Id
может быть определен как строка "5e25a8e88170fb0f8ce90f71"
, номер 345924
или объект { _id: "5e25a8e88170fb0f8ce90f71" }
.
Удаление нескольких документов с помощью model.remove()
Функция Model.remove()
полезна для удаления всех документов, соответствующих заданным критериям.
require('dotenv').config();
const mongoose = require('mongoose');
mongoose.connect(
process.env.MONGO_URI,
{ useNewUrlParser: true, useUnifiedTopology: true },
function (err) {
if (err) throw err;
console.log('БД подключена');
}
);
var userSchema = new mongoose.Schema({
name: { type: String, default: 'Анонимный' },
age: { type: Number, min: 18, index: true },
favoriteFoods: [String],
});
const UserModel = mongoose.model('UserModel', userSchema);
UserModel.remove({ name: 'Tom' }, function (err, data) {
if (err) return console.log(err);
console.log('Удалено ' + data.n + ' документов из БД');
});
Примечание: Метод remove()
возвращает не удаленный документ, а объект JSON, содержащий результат операции и количество удаленных элементов.
Цепочка помощников по поисковым запросам для сужения результатов поиска
Если вы не передадите функцию обратнного вызова в качестве последнего аргумента в методе Model.find()
(или в других методах поиска), то запрос не будет выполнен. Запрос можно сохранить в переменной для последующего использования. Этот тип объектов позволяет построить запрос с использованием цепочечного синтаксиса. Фактический поиск в БД выполняется, когда вы окончательно прицепите метод .exec()
. Вы всегда должны передавать свою функцию обратного вызова этому последнему методу. Есть много помощников запроса, здесь вы узнаете о самых «известных» из них.
require('dotenv').config();
const mongoose = require('mongoose');
mongoose.connect(
process.env.MONGO_URI,
{ useNewUrlParser: true, useUnifiedTopology: true },
function (err) {
if (err) throw err;
console.log('БД подключена');
}
);
var userSchema = new mongoose.Schema({
name: { type: String, default: 'Анонимный' },
age: { type: Number, min: 18, index: true },
favoriteFoods: [String],
});
const UserModel = mongoose.model('UserModel', userSchema);
UserModel.find({ favoriteFoods: 'чипсы' })
.sort({ name: 'asc' })
.limit(2)
.select('-age')
.exec(function (err, user) {
if (err) return console.error(err);
console.log('Найдены пользователи, которые любят чипсы');
console.log(user);
});
Вышеприведенный код находит в базе данных людей, которые любят чипсы
, сортирует их по имени, ограничивает результаты поиска двумя документами и при выводе результатов скрывает их возраст. Результат выводится в виде массива документов.
sort({ name: 'asc' })
— Устанавливает порядок сортировки по полю name
. Допустимые значения для сортировки: asc
, ascending
или 1
— сортировка по возрастанию; desc
, descending
или -1
— сортировка по убыванию. В качестве параметра сортировки можно задавать не только объект, но и строку. В этом случае должен быть разделенный пробелом список имен полей. Если перед именем поля не стоит знак «минус», то порядок сортировки будет возрастающим, если знак «минус» стоит, то порядок сортировки будет убывающим.
limit(2)
— Ограничивает максимальное количество документов, возвращаемых в запросе, — двумя.
select('-age')
— Указывает, что поле age
(указывающее возраст) должно быть исключено из выводимого результата. На это указывает знак «минус» перед именем поля.
exec(callback)
— Выполняет запрос.
Более подробно о помощниках запросов смотри здесь
Используемые ресурсы:
- https://www.freecodecamp.org/learn/apis-and-microservices/mongodb-and-mongoose/
-
https://mongoosejs.com/
-
https://code.tutsplus.com/ru/articles/an-introduction-to-mongoose-for-mongodb-and-nodejs—cms-29527
-
-
https://developer.mozilla.org/ru/docs/Learn/Server-side/Express_Nodejs/mongoose
-
https://metanit.com/nosql/mongodb/
-
http://www.coldfox.ru/article/5be022d49227d914a1c83fe3/%D0%9F%D0%BE%D0%B4%D1%80%D0%BE%D0%B1%D0%BD%D0%BE%D0%B5-%D1%80%D1%83%D0%BA%D0%BE%D0%B2%D0%BE%D0%B4%D1%81%D1%82%D0%B2%D0%BE-%D0%BF%D0%BE-MongoDB-Mongoose
Mongoose is a library that makes MongoDB easier to use. It does two things:
- It gives structure to MongoDB Collections
- It gives you helpful methods to use
In this article, we’ll go through:
- The basics of using Mongoose
- Mongoose subdocuments
- Mongoose population
By the end of the article, you should be able to use Mongoose without problems.
Prerequisites
I assume you have done the following:
- You have installed MongoDB on your computer
- You know how to set up a local MongoDB connection
- You know how to see the data you have in your database
- You know what «collections» are in MongoDB
If you don’t know any of these, please read «How to set up a local MongoDB connection» before you continue.
I also assume you know how to use MongoDB to create a simple CRUD app. If you don’t know how to do this, please read «How to build a CRUD app with Node, Express, and MongoDB» before you continue.
Here, you’ll learn how to:
- Connect to the database
- Create a Model
- Create a Document
- Find a Document
- Update a Document
- Delete a Document
Connecting to a database
First, you need to download Mongoose.
npm install mongoose --save
You can connect to a database with the connect
method. Let’s say we want to connect to a database called street-fighters
. Here’s the code you need:
const mongoose = require('mongoose')
const url = 'mongodb://127.0.0.1:27017/street-fighters'
mongoose.connect(url, { useNewUrlParser: true })
We want to know whether our connection has succeeded or failed. This helps us with debugging.
To check whether the connection has succeeded, we can use the open
event. To check whether the connection failed, we use the error
event.
const db = mongoose.connection
db.once('open', _ => {
console.log('Database connected:', url)
})
db.on('error', err => {
console.error('connection error:', err)
})
Try connecting to the database. You should see a log like this:
Creating a Model
In Mongoose, you need to use models to create, read, update, or delete items from a MongoDB collection.
To create a Model, you need to create a Schema. A Schema lets you define the structure of an entry in the collection. This entry is also called a document.
Here’s how you create a schema:
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const schema = new Schema({
// ...
})
You can use 10 different kinds of values in a Schema. Most of the time, you’ll use these six:
- String
- Number
- Boolean
- Array
- Date
- ObjectId
Let’s put this into practice.
Say we want to create characters for our Street Fighter database.
In Mongoose, it’s a normal practice to put each model in its own file. So we will create a Character.js
file first. This Character.js
file will be placed in the models
folder.
project/
|- models/
|- Character.js
In Character.js
, we create a characterSchema
.
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const characterSchema = new Schema({
// ...
})
Let’s say we want to save two things into the database:
- Name of the character
- Name of their ultimate move
Both can be represented with Strings.
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const characterSchema = new Schema({
name: String,
ultimate: String
})
Once we’ve created characterSchema
, we can use mongoose’s model
method to create the model.
module.exports = mongoose.model('Character', characterSchema)
Creating a document
Let’s say you have a file called index.js
. This is where we’ll perform Mongoose operations for this tutorial.
project/
|- index.js
|- models/
|- Character.js
First, you need to load the Character model. You can do this with require
.
const Character = require('./models/Character')
Let’s say you want to create a character called Ryu. Ryu has an ultimate move called «Shinku Hadoken».
To create Ryu, you use the new
, followed by your model. In this case, it’s new Character
.
const ryu = new Character ({
name: 'Ryu',
ultimate: 'Shinku Hadoken'
})
new Character
creates the character in memory. It has not been saved to the database yet. To save to the database, you can run the save
method.
ryu.save(function (error, document) {
if (error) console.error(error)
console.log(document)
})
If you run the code above, you should see this in the console.
Promises and Async/await
Mongoose supports promises. It lets you write nicer code like this:
// This does the same thing as above
function saveCharacter (character) {
const c = new Character(character)
return c.save()
}
saveCharacter({
name: 'Ryu',
ultimate: 'Shinku Hadoken'
})
.then(doc => { console.log(doc) })
.catch(error => { console.error(error) })
You can also use the await
keyword if you have an asynchronous function.
If the Promise or Async/Await code looks foreign to you, I recommend reading «JavaScript async and await» before continuing with this tutorial.
async function runCode() {
const ryu = new Character({
name: 'Ryu',
ultimate: 'Shinku Hadoken'
})
const doc = await ryu.save()
console.log(doc)
}
runCode()
.catch(error => { console.error(error) })
Note: I’ll use the async/await format for the rest of the tutorial.
Uniqueness
Mongoose adds a new character to the database each time you use new Character
and save
. If you run the code(s) above three times, you’d expect to see three Ryus in the database.
We don’t want to have three Ryus in the database. We want to have ONE Ryu only. To do this, we can use the unique option.
const characterSchema = new Schema({
name: { type: String, unique: true },
ultimate: String
})
The unique
option creates a unique index. It ensures that we cannot have two documents with the same value (for name
in this case).
For unique
to work properly, you need to clear the Characters collection. To clear the Characters collection, you can use this:
await Character.deleteMany({})
Try to add two Ryus into the database now. You’ll get an E11000 duplicate key error
. You won’t be able to save the second Ryu.
Let’s add another character into the database before we continue the rest of the tutorial.
const ken = new Character({
name: 'Ken',
ultimate: 'Guren Enjinkyaku'
})
await ken.save()
Finding a document
Mongoose gives you two methods to find stuff from MongoDB.
findOne
: Gets one document.find
: Gets an array of documents
findOne
findOne
returns the first document it finds. You can specify any property to search for. Let’s search for Ryu
:
const ryu = await Character.findOne({ name: 'Ryu' })
console.log(ryu)
find
find
returns an array of documents. If you specify a property to search for, it’ll return documents that match your query.
const chars = await Character.find({ name: 'Ryu' })
console.log(chars)
If you did not specify any properties to search for, it’ll return an array that contains all documents in the collection.
const chars = await Character.find()
console.log(chars)
Updating a document
Let’s say Ryu has three special moves:
- Hadoken
- Shoryuken
- Tatsumaki Senpukyaku
We want to add these special moves into the database. First, we need to update our CharacterSchema
.
const characterSchema = new Schema({
name: { type: String, unique: true },
specials: Array,
ultimate: String
})
Then, we use one of these two ways to update a character:
- Use
findOne
, then usesave
- Use
findOneAndUpdate
findOne and save
First, we use findOne
to get Ryu.
const ryu = await Character.findOne({ name: 'Ryu' })
console.log(ryu)
Then, we update Ryu to include his special moves.
const ryu = await Character.findOne({ name: 'Ryu' })
ryu.specials = [
'Hadoken',
'Shoryuken',
'Tatsumaki Senpukyaku'
]
After we modified ryu
, we run save
.
const ryu = await Character.findOne({ name: 'Ryu' })
ryu.specials = [
'Hadoken',
'Shoryuken',
'Tatsumaki Senpukyaku'
]
const doc = await ryu.save()
console.log(doc)
findOneAndUpdate
findOneAndUpdate
is the same as MongoDB’s findOneAndModify
method.
Here, you search for Ryu and pass the fields you want to update at the same time.
// Syntax
await findOneAndUpdate(filter, update)
// Usage
const doc = await Character.findOneAndUpdate(
{ name: 'Ryu' },
{
specials: [
'Hadoken',
'Shoryuken',
'Tatsumaki Senpukyaku'
]
})
console.log(doc)
Difference between findOne + save vs findOneAndUpdate
Two major differences.
First, the syntax for findOne` + `save
is easier to read than findOneAndUpdate
.
Second, findOneAndUpdate
does not trigger the save
middleware.
I’ll choose findOne
+ save
over findOneAndUpdate
anytime because of these two differences.
Deleting a document
There are two ways to delete a character:
findOne
+remove
findOneAndDelete
Using findOne + remove
const ryu = await Character.findOne({ name: 'Ryu' })
const deleted = await ryu.remove()
Using findOneAndDelete
const deleted = await Character.findOneAndDelete({ name: 'Ken' })
Subdocuments
In Mongoose, subdocuments are documents that are nested in other documents. You can spot a subdocument when a schema is nested in another schema.
Note: MongoDB calls subdocuments embedded documents.
const childSchema = new Schema({
name: String
});
const parentSchema = new Schema({
// Single subdocument
child: childSchema,
// Array of subdocuments
children: [ childSchema ]
});
In practice, you don’t have to create a separate childSchema
like the example above. Mongoose helps you create nested schemas when you nest an object in another object.
// This code is the same as above
const parentSchema = new Schema({
// Single subdocument
child: { name: String },
// Array of subdocuments
children: [{name: String }]
});
In this section, you will learn to:
- Create a schema that includes a subdocument
- Create documents that contain subdocuments
- Update subdocuments that are arrays
- Update a single subdocument
Updating characterSchema
Let’s say we want to create a character called Ryu. Ryu has three special moves.
- Hadoken
- Shinryuken
- Tatsumaki Senpukyaku
Ryu also has one ultimate move called:
- Shinku Hadoken
We want to save the names of each move. We also want to save the keys required to execute that move.
Here, each move is a subdocument.
const characterSchema = new Schema({
name: { type: String, unique: true },
// Array of subdocuments
specials: [{
name: String,
keys: String
}]
// Single subdocument
ultimate: {
name: String,
keys: String
}
})
You can also use the childSchema syntax if you wish to. It makes the Character schema easier to understand.
const moveSchema = new Schema({
name: String,
keys: String
})
const characterSchema = new Schema({
name: { type: String, unique: true },
// Array of subdocuments
specials: [moveSchema],
// Single subdocument
ultimate: moveSchema
})
Creating documents that contain subdocuments
There are two ways to create documents that contain subdocuments:
- Pass a nested object into
new Model
- Add properties into the created document.
Method 1: Passing the entire object
For this method, we construct a nested object that contains both Ryu’s name and his moves.
const ryu = {
name: 'Ryu',
specials: [{
name: 'Hadoken',
keys: '↓ ↘ → P'
}, {
name: 'Shoryuken',
keys: '→ ↓ ↘ → P'
}, {
name: 'Tatsumaki Senpukyaku',
keys: '↓ ↙ ↠K'
}],
ultimate: {
name: 'Shinku Hadoken',
keys: '↓ ↘ → ↓ ↘ → P'
}
}
Then, we pass this object into new Character
.
const char = new Character(ryu)
const doc = await char.save()
console.log(doc)
Method 2: Adding subdocuments later
For this method, we create a character with new Character
first.
const ryu = new Character({ name: 'Ryu' })
Then, we edit the character to add special moves:
const ryu = new Character({ name: 'Ryu' })
const ryu.specials = [{
name: 'Hadoken',
keys: '↓ ↘ → P'
}, {
name: 'Shoryuken',
keys: '→ ↓ ↘ → P'
}, {
name: 'Tatsumaki Senpukyaku',
keys: '↓ ↙ ↠K'
}]
Then, we edit the character to add the ultimate move:
const ryu = new Character({ name: 'Ryu' })
// Adds specials
const ryu.specials = [{
name: 'Hadoken',
keys: '↓ ↘ → P'
}, {
name: 'Shoryuken',
keys: '→ ↓ ↘ → P'
}, {
name: 'Tatsumaki Senpukyaku',
keys: '↓ ↙ ↠K'
}]
// Adds ultimate
ryu.ultimate = {
name: 'Shinku Hadoken',
keys: '↓ ↘ → ↓ ↘ → P'
}
Once we’re satisfied with ryu
, we run save
.
const ryu = new Character({ name: 'Ryu' })
// Adds specials
const ryu.specials = [{
name: 'Hadoken',
keys: '↓ ↘ → P'
}, {
name: 'Shoryuken',
keys: '→ ↓ ↘ → P'
}, {
name: 'Tatsumaki Senpukyaku',
keys: '↓ ↙ ↠K'
}]
// Adds ultimate
ryu.ultimate = {
name: 'Shinku Hadoken',
keys: '↓ ↘ → ↓ ↘ → P'
}
const doc = await ryu.save()
console.log(doc)
Updating array subdocuments
The easiest way to update subdocuments is:
- Use
findOne
to find the document - Get the array
- Change the array
- Run
save
For example, let’s say we want to add Jodan Sokutou Geri
to Ryu’s special moves. The keys for Jodan Sokutou Geri
are ↓ ↘ → K
.
First, we find Ryu with findOne
.
const ryu = await Characters.findOne({ name: 'Ryu' })
Mongoose documents behave like regular JavaScript objects. We can get the specials
array by writing ryu.specials
.
const ryu = await Characters.findOne({ name: 'Ryu' })
const specials = ryu.specials
console.log(specials)
This specials
array is a normal JavaScript array.
const ryu = await Characters.findOne({ name: 'Ryu' })
const specials = ryu.specials
console.log(Array.isArray(specials)) // true
We can use the push
method to add a new item into specials
,
const ryu = await Characters.findOne({ name: 'Ryu' })
ryu.specials.push({
name: 'Jodan Sokutou Geri',
keys: '↓ ↘ → K'
})
After updating specials
, we run save
to save Ryu to the database.
const ryu = await Characters.findOne({ name: 'Ryu' })
ryu.specials.push({
name: 'Jodan Sokutou Geri',
keys: '↓ ↘ → K'
})
const updated = await ryu.save()
console.log(updated)
Updating a single subdocument
It’s even easier to update single subdocuments. You can edit the document directly like a normal object.
Let’s say we want to change Ryu’s ultimate name from Shinku Hadoken to Dejin Hadoken. What we do is:
- Use
findOne
to get Ryu. - Change the
name
inultimate
- Run
save
const ryu = await Characters.findOne({ name: 'Ryu' })
ryu.ultimate.name = 'Dejin Hadoken'
const updated = await ryu.save()
console.log(updated)
Population
MongoDB documents have a size limit of 16MB. This means you can use subdocuments (or embedded documents) if they are small in number.
For example, Street Fighter characters have a limited number of moves. Ryu only has 4 special moves. In this case, it’s okay to use embed moves directly into Ryu’s character document.
But if you have data that can contain an unlimited number of subdocuments, you need to design your database differently.
One way is to create two separate models and combine them with populate.
Creating the models
Let’s say you want to create a blog. And you want to store the blog content with MongoDB. Each blog has a title, content, and comments.
Your first schema might look like this:
const blogPostSchema = new Schema({
title: String,
content: String,
comments: [{
comment: String
}]
})
module.exports = mongoose.model('BlogPost', blogPostSchema)
There’s a problem with this schema.
A blog post can have an unlimited number of comments. If a blog post explodes in popularity and comments swell up, the document might exceed the 16MB limit imposed by MongoDB.
This means we should not embed comments in blog posts. We should create a separate collection for comments.
const comments = new Schema({
comment: String
})
module.exports = mongoose.model('Comment', commentSchema)
In Mongoose, we can link up the two models with Population.
To use Population, we need to:
- Set
type
of a property toSchema.Types.ObjectId
- Set
ref
to the model we want to link too.
Here, we want comments
in blogPostSchema
to link to the Comment collection. This is the schema we’ll use:
const blogPostSchema = new Schema({
title: String,
content: String,
comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }]
})
module.exports = mongoose.model('BlogPost', blogPostSchema)
Creating a blog post
Let’s say you want to create a blog post. To create the blog post, you use new BlogPost
.
const blogPost = new BlogPost({
title: 'Weather',
content: `How's the weather today?`
})
A blog post can have zero comments. We can save this blog post with save
.
const doc = await blogPost.save()
console.log(doc)
Now let’s say we want to create a comment for the blog post. To do this, we create and save the comment.
const comment = new Comment({
comment: `It's damn hot today`
})
const savedComment = await comment.save()
console.log(savedComment)
Notice the saved comment has an _id
attribute. We need to add this _id
attribute into the blog post’s comments
array. This creates the link.
// Saves comment to Database
const savedComment = await comment.save()
// Adds comment to blog post
// Then saves blog post to database
const blogPost = await BlogPost.findOne({ title: 'Weather' })
blogPost.comments.push(savedComment._id)
const savedPost = await blogPost.save()
console.log(savedPost)
Searching blog posts and their comments
If you tried to search for the blog post, you’ll see the blog post has an array of comment IDs.
const blogPost = await BlogPost.findOne({ title: 'Weather' })
console.log(blogPost)
There are four ways to get comments.
- Mongoose population
- Manual way #1
- Manual way #2
- Manual way #3
Mongoose Population
Mongoose allows you to fetch linked documents with the populate
method. What you need to do is call .populate
when you execute with findOne
.
When you call populate, you need to pass in the key
of the property you want to populate. In this case, the key
is comments
. (Note: Mongoose calls this key
a «path»).
const blogPost = await BlogPost.findOne({ title: 'Weather' })
.populate('comments')
console.log(blogPost)
Manual way (method 1)
Without Mongoose Populate, you need to find the comments manually. First, you need to get the array of comments.
const blogPost = await BlogPost.findOne({ title: 'Weather' })
.populate('comments')
const commentIDs = blogPost.comments
Then, you loop through commentIDs
to find each comment. If you go with this method, it’s slightly faster to use Promise.all
.
const commentPromises = commentIDs.map(_id => {
return Comment.findOne({ _id })
})
const comments = await Promise.all(commentPromises)
console.log(comments)
Manual way (method 2)
Mongoose gives you an $in
operator. You can use this $in
operator to find all comments within an array. This syntax takes a little effort to get used to.
If I had to do the manual way, I’d prefer Manual #1 over this.
const commentIDs = blogPost.comments
const comments = await Comment.find({
'_id': { $in: commentIDs }
})
console.log(comments)
Manual way (method 3)
For the third method, we need to change the schema. When we save a comment, we link the comment to the blog post.
// Linking comments to blog post
const commentSchema = new Schema({
comment: String
blogPost: [{ type: Schema.Types.ObjectId, ref: 'BlogPost' }]
})
module.exports = mongoose.model('Comment', commentSchema)
You need to save the comment into the blog post, and the blog post id into the comment.
const blogPost = await BlogPost.findOne({ title: 'Weather' })
// Saves comment
const comment = new Comment({
comment: `It's damn hot today`,
blogPost: blogPost._id
})
const savedComment = comment.save()
// Links blog post to comment
blogPost.comments.push(savedComment._id)
await blogPost.save()
Once you do this, you can search the Comments collection for comments that match your blog post’s id.
// Searches for comments
const blogPost = await BlogPost.findOne({ title: 'Weather' })
const comments = await Comment.find({ _id: blogPost._id })
console.log(comments)
I’d prefer Manual #3 over Manual #1 and Manual #2.
And Population beats all three manual methods.
Quick Summary
You learned to use Mongoose on three different levels in this article:
- Basic Mongoose
- Mongoose subdocuments
- Mongoose population
That’s it!
Thanks for reading. This article was originally posted on my blog. Sign up for my newsletter if you want more articles to help you become a better frontend developer.
Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started