Mongoose mongodb руководство

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. Эти типы следующие:

  1. String
  2. Number
  3. Date
  4. Buffer
  5. Boolean
  6. Mixed
  7. ObjectId (* уникальный идентификатор объекта, первичный ключ, _id)
  8. 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,  чтобы продемонстрировать вам возможности определения схем. Каждый элемент этого массива содержит свойства summarydetailnumberOfStars и 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, добавив проверку данных следующих свойств: firstNametwitterfacebook и 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 (* отсутствует фамилия).

Для свойств twitterfacebook и linkedin используются подобные пользовательские валидаторы. Они проверяются на соответствие начала их значений соответствующему доменному имени социальных сетей. Поскольку это необязательные для заполнения поля, валидатор применяется только в случае поступления данных для этого свойства.

Поиск и обновление данных

Введение в Mongoose не было бы завершенным без примера поиска записи и обновления одного или более свойств этого объекта.

Mongoose предоставляет несколько различных функций для поиска данных определенной модели. Эти функции следующие: findfindOne и 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 таккже можно «прицепить» к другим методам запроса, например, whereandorlimitsortany и т.д.

Давайте распространим наш предыдущий пример, ограничив количество результатов до пяти первых книг и отсортировав их по дате создания по убыванию. Результатом будут первые пять наиболее новых книг, содержащих в названии строку ‘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 можно использовать три пути:

  1. Для создания базы данных MongoDB и разработки приложения использовать собственный компьютер. Для этого вы должны установить сервер Node и сервер базы данных MongoDB на своем ПК.
  2. Для создания базы данных MongoDB использовать облачный сервис MongoDB Atlas, а приложение разрабатывать и запускать на локальном ПК. Этот способ будет рассмотрен в данной статье.
  3. Для создания базы данных 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

  • Mongoose для MongoDb

  • 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:

  1. It gives structure to MongoDB Collections
  2. It gives you helpful methods to use

In this article, we’ll go through:

  1. The basics of using Mongoose
  2. Mongoose subdocuments
  3. 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:

  1. You have installed MongoDB on your computer
  2. You know how to set up a local MongoDB connection
  3. You know how to see the data you have in your database
  4. 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:

  1. Connect to the database
  2. Create a Model
  3. Create a Document
  4. Find a Document
  5. Update a Document
  6. 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:

Connected to a database.

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:

  1. Name of the character
  2. 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.

Ryu saved to the database.

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.

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.

Duplicated key error.

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()

Database contains two characters.

Finding a document

Mongoose gives you two methods to find stuff from MongoDB.

  1. findOne: Gets one document.
  2. 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)

Found Ryu from the database.

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)

Combed through the database and found one character with the name Ryu.

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)

Found two characters in the database.

Updating a document

Let’s say Ryu has three special moves:

  1. Hadoken
  2. Shoryuken
  3. 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:

  1. Use findOne, then use save
  2. 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)

Updated Ryu.

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)

Updated Ryu.

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:

  1. findOne + remove
  2. 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:

  1. Create a schema that includes a subdocument
  2. Create documents that contain subdocuments
  3. Update subdocuments that are arrays
  4. Update a single subdocument

Updating characterSchema

Let’s say we want to create a character called Ryu. Ryu has three special moves.

  1. Hadoken
  2. Shinryuken
  3. Tatsumaki Senpukyaku

Ryu also has one ultimate move called:

  1. 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:

  1. Pass a nested object into new Model
  2. 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)

Image of Ryu's document.

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)

Image of Ryu's document.

Updating array subdocuments

The easiest way to update subdocuments is:

  1. Use findOne to find the document
  2. Get the array
  3. Change the array
  4. 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)

Log of 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)

Ryu updated with Jodan Sokutou Geri

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:

  1. Use findOne to get Ryu.
  2. Change the name in ultimate
  3. Run save
const ryu = await Characters.findOne({ name: 'Ryu' })
ryu.ultimate.name = 'Dejin Hadoken'

const updated = await ryu.save()
console.log(updated)

Ryu document with Dejin Hadoken.

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.

Ryu's 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:

  1. Set type of a property to Schema.Types.ObjectId
  2. 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)

Created a blog post document without comments.

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)

Created and saved a comment.

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)

blog-post-with-comments

Blog post with comments.

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)

Found blog post contains comment ids.

There are four ways to get comments.

  1. Mongoose population
  2. Manual way #1
  3. Manual way #2
  4. 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)

Comments populated by Mongoose.

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)

Comments found.

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)

Comments found.

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)

Comments found.

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:

  1. Basic Mongoose
  2. Mongoose subdocuments
  3. 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

Понравилась статья? Поделить с друзьями:
  • Тримедат инструкция по применению взрослым в таблетках от чего цена
  • Daeng gi meo ri средство против выпадения инструкция
  • Соблюдайте руководство по эксплуатации
  • Супракс инструкция по применению цена таблетки 400мг
  • Инструкция для оказания первой помощи для автоаптечки