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 и т.д.

Удачной охоты (да простят меня мангусты)!

Schemas

If you haven’t yet done so, please take a minute to read the quickstart to get an idea of how Mongoose works.
If you are migrating from 6.x to 7.x please take a moment to read the migration guide.

  • Defining your schema
  • Creating a model
  • Ids
  • Instance methods
  • Statics
  • Query Helpers
  • Indexes
  • Virtuals
  • Aliases
  • Options
  • With ES6 Classes
  • Pluggable
  • Further Reading

Defining your schema

Everything in Mongoose starts with a Schema. Each schema maps to a MongoDB
collection and defines the shape of the documents within that collection.

import mongoose from 'mongoose';
const { Schema } = mongoose;

const blogSchema = new Schema({
  title: String, // String is shorthand for {type: String}
  author: String,
  body: String,
  comments: [{ body: String, date: Date }],
  date: { type: Date, default: Date.now },
  hidden: Boolean,
  meta: {
    votes: Number,
    favs: Number
  }
});

If you want to add additional keys later, use the
Schema#add method.

Each key in our code blogSchema defines a property in our documents which
will be cast to its associated SchemaType.
For example, we’ve defined a property title which will be cast to the
String SchemaType and property date
which will be cast to a Date SchemaType.

Notice above that if a property only requires a type, it can be specified using
a shorthand notation (contrast the title property above with the date
property).

Keys may also be assigned nested objects containing further key/type definitions
like the meta property above. This will happen whenever a key’s value is a POJO
that doesn’t have a type property.

In these cases, Mongoose only creates actual schema paths for leaves
in the tree. (like meta.votes and meta.favs above),
and the branches do not have actual paths. A side-effect of this is that meta
above cannot have its own validation. If validation is needed up the tree, a path
needs to be created up the tree — see the Subdocuments section
for more information on how to do this. Also read the Mixed
subsection of the SchemaTypes guide for some gotchas.

The permitted SchemaTypes are:

  • String
  • Number
  • Date
  • Buffer
  • Boolean
  • Mixed
  • ObjectId
  • Array
  • Decimal128
  • Map
  • UUID

Read more about SchemaTypes here.

Schemas not only define the structure of your document and casting of
properties, they also define document instance methods,
static Model methods, compound indexes,
and document lifecycle hooks called middleware.

Creating a model

To use our schema definition, we need to convert our blogSchema into a
Model we can work with.
To do so, we pass it into mongoose.model(modelName, schema):

const Blog = mongoose.model('Blog', blogSchema);
// ready to go!

Ids

By default, Mongoose adds an _id property to your schemas.

const schema = new Schema();

schema.path('_id'); // ObjectId { ... }

When you create a new document with the automatically added
_id property, Mongoose creates a new _id of type ObjectId
to your document.

const Model = mongoose.model('Test', schema);

const doc = new Model();
doc._id instanceof mongoose.Types.ObjectId; // true

You can also overwrite Mongoose’s default _id with your
own _id. Just be careful: Mongoose will refuse to save a
document that doesn’t have an _id, so you’re responsible
for setting _id if you define your own _id path.

const schema = new Schema({ _id: Number });
const Model = mongoose.model('Test', schema);

const doc = new Model();
await doc.save(); // Throws "document must have an _id before saving"

doc._id = 1;
await doc.save(); // works

Instance methods

Instances of Models are documents. Documents have
many of their own built-in instance methods.
We may also define our own custom document instance methods.

// define a schema
const animalSchema = new Schema({ name: String, type: String },
  {
  // Assign a function to the "methods" object of our animalSchema through schema options.
  // By following this approach, there is no need to create a separate TS type to define the type of the instance functions.
    methods: {
      findSimilarTypes(cb) {
        return mongoose.model('Animal').find({ type: this.type }, cb);
      }
    }
  });

// Or, assign a function to the "methods" object of our animalSchema
animalSchema.methods.findSimilarTypes = function(cb) {
  return mongoose.model('Animal').find({ type: this.type }, cb);
};

Now all of our animal instances have a findSimilarTypes method available
to them.

const Animal = mongoose.model('Animal', animalSchema);
const dog = new Animal({ type: 'dog' });

dog.findSimilarTypes((err, dogs) => {
  console.log(dogs); // woof
});
  • Overwriting a default mongoose document method may lead to unpredictable results. See this for more details.
  • The example above uses the Schema.methods object directly to save an instance method. You can also use the Schema.method() helper as described here.
  • Do not declare methods using ES6 arrow functions (=>). Arrow functions explicitly prevent binding this, so your method will not have access to the document and the above examples will not work.

Statics

You can also add static functions to your model. There are three equivalent
ways to add a static:

  • Add a function property to the second argument of the schema-constructor (statics)
  • Add a function property to schema.statics
  • Call the Schema#static() function
// define a schema
const animalSchema = new Schema({ name: String, type: String },
  {
  // Assign a function to the "statics" object of our animalSchema through schema options.
  // By following this approach, there is no need to create a separate TS type to define the type of the statics functions.
    statics: {
      findByName(name) {
        return this.find({ name: new RegExp(name, 'i') });
      }
    }
  });

// Or, Assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function(name) {
  return this.find({ name: new RegExp(name, 'i') });
};
// Or, equivalently, you can call `animalSchema.static()`.
animalSchema.static('findByBreed', function(breed) { return this.find({ breed }); });

const Animal = mongoose.model('Animal', animalSchema);
let animals = await Animal.findByName('fido');
animals = animals.concat(await Animal.findByBreed('Poodle'));

Do not declare statics using ES6 arrow functions (=>). Arrow functions explicitly prevent binding this, so the above examples will not work because of the value of this.

Query Helpers

You can also add query helper functions, which are like instance methods
but for mongoose queries. Query helper methods let you extend mongoose’s
chainable query builder API.

// define a schema
const animalSchema = new Schema({ name: String, type: String },
  {
  // Assign a function to the "query" object of our animalSchema through schema options.
  // By following this approach, there is no need to create a separate TS type to define the type of the query functions.
    query: {
      byName(name) {
        return this.where({ name: new RegExp(name, 'i') });
      }
    }
  });

// Or, Assign a function to the "query" object of our animalSchema
animalSchema.query.byName = function(name) {
  return this.where({ name: new RegExp(name, 'i') });
};

const Animal = mongoose.model('Animal', animalSchema);

Animal.find().byName('fido').exec((err, animals) => {
  console.log(animals);
});

Animal.findOne().byName('fido').exec((err, animal) => {
  console.log(animal);
});

Indexes

MongoDB supports secondary indexes.
With mongoose, we define these indexes within our Schema at the path level or the schema level.
Defining indexes at the schema level is necessary when creating
compound indexes.

const animalSchema = new Schema({
  name: String,
  type: String,
  tags: { type: [String], index: true } // path level
});

animalSchema.index({ name: 1, type: -1 }); // schema level

See SchemaType#index() for other index options.

When your application starts up, Mongoose automatically calls createIndex for each defined index in your schema.
Mongoose will call createIndex for each index sequentially, and emit an ‘index’ event on the model when all the createIndex calls succeeded or when there was an error.
While nice for development, it is recommended this behavior be disabled in production since index creation can cause a significant performance impact.
Disable the behavior by setting the autoIndex option of your schema to false, or globally on the connection by setting the option autoIndex to false.

mongoose.connect('mongodb://user:pass@127.0.0.1:port/database', { autoIndex: false });
// or
mongoose.createConnection('mongodb://user:pass@127.0.0.1:port/database', { autoIndex: false });
// or
mongoose.set('autoIndex', false);
// or
animalSchema.set('autoIndex', false);
// or
new Schema({ /* ... */ }, { autoIndex: false });

Mongoose will emit an index event on the model when indexes are done
building or an error occurred.

// Will cause an error because mongodb has an _id index by default that
// is not sparse
animalSchema.index({ _id: 1 }, { sparse: true });
const Animal = mongoose.model('Animal', animalSchema);

Animal.on('index', error => {
  // "_id index cannot be sparse"
  console.log(error.message);
});

See also the Model#ensureIndexes method.

Virtuals

Virtuals are document properties that
you can get and set but that do not get persisted to MongoDB. The getters
are useful for formatting or combining fields, while setters are useful for
de-composing a single value into multiple values for storage.

// define a schema
const personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
});

// compile our model
const Person = mongoose.model('Person', personSchema);

// create a document
const axl = new Person({
  name: { first: 'Axl', last: 'Rose' }
});

Suppose you want to print out the person’s full name. You could do it yourself:

console.log(axl.name.first + ' ' + axl.name.last); // Axl Rose

But concatenating the first and
last name every time can get cumbersome.
And what if you want to do some extra processing on the name, like
removing diacritics? A
virtual property getter lets you
define a fullName property that won’t get persisted to MongoDB.

// That can be done either by adding it to schema options:
const personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
}, {
  virtuals: {
    fullName: {
      get() {
        return this.name.first + ' ' + this.name.last;
      }
    }
  }
});

// Or by using the virtual method as following:
personSchema.virtual('fullName').get(function() {
  return this.name.first + ' ' + this.name.last;
});

Now, mongoose will call your getter function every time you access the
fullName property:

console.log(axl.fullName); // Axl Rose

If you use toJSON() or toObject() mongoose will not include virtuals
by default. This includes the output of calling JSON.stringify()
on a Mongoose document, because JSON.stringify() calls toJSON().
Pass { virtuals: true } to either
toObject() or toJSON().

You can also add a custom setter to your virtual that will let you set both
first name and last name via the fullName virtual.

// Again that can be done either by adding it to schema options:
const personSchema = new Schema({
  name: {
    first: String,
    last: String
  }
}, {
  virtuals: {
    fullName: {
      get() {
        return this.name.first + ' ' + this.name.last;
      },
      set(v) {
        this.name.first = v.substr(0, v.indexOf(' '));
        this.name.last = v.substr(v.indexOf(' ') + 1);
      }
    }
  }
});

// Or by using the virtual method as following:
personSchema.virtual('fullName').
  get(function() {
    return this.name.first + ' ' + this.name.last;
  }).
  set(function(v) {
    this.name.first = v.substr(0, v.indexOf(' '));
    this.name.last = v.substr(v.indexOf(' ') + 1);
  });

axl.fullName = 'William Rose'; // Now `axl.name.first` is "William"

Virtual property setters are applied before other validation. So the example
above would still work even if the first and last name fields were
required.

Only non-virtual properties work as part of queries and for field selection.
Since virtuals are not stored in MongoDB, you can’t query with them.

You can learn more about virtuals here.

Aliases

Aliases are a particular type of virtual where the getter and setter
seamlessly get and set another property. This is handy for saving network
bandwidth, so you can convert a short property name stored in the database
into a longer name for code readability.

const personSchema = new Schema({
  n: {
    type: String,
    // Now accessing `name` will get you the value of `n`, and setting `name` will set the value of `n`
    alias: 'name'
  }
});

// Setting `name` will propagate to `n`
const person = new Person({ name: 'Val' });
console.log(person); // { n: 'Val' }
console.log(person.toObject({ virtuals: true })); // { n: 'Val', name: 'Val' }
console.log(person.name); // "Val"

person.name = 'Not Val';
console.log(person); // { n: 'Not Val' }

You can also declare aliases on nested paths. It is easier to use nested
schemas and subdocuments, but you can also declare
nested path aliases inline as long as you use the full nested path
nested.myProp as the alias.

Options

Schemas have a few configurable options which can be passed to the
constructor or to the set method:

new Schema({ /* ... */ }, options);

// or

const schema = new Schema({ /* ... */ });
schema.set(option, value);

Valid options:

  • autoIndex
  • autoCreate
  • bufferCommands
  • bufferTimeoutMS
  • capped
  • collection
  • discriminatorKey
  • id
  • _id
  • minimize
  • read
  • writeConcern
  • shardKey
  • statics
  • strict
  • strictQuery
  • toJSON
  • toObject
  • typeKey
  • validateBeforeSave
  • versionKey
  • optimisticConcurrency
  • collation
  • timeseries
  • selectPopulatedPaths
  • skipVersioning
  • timestamps
  • storeSubdocValidationError
  • methods
  • query

option: autoIndex

By default, Mongoose’s init() function
creates all the indexes defined in your model’s schema by calling
Model.createIndexes()
after you successfully connect to MongoDB. Creating indexes automatically is
great for development and test environments. But index builds can also create
significant load on your production database. If you want to manage indexes
carefully in production, you can set autoIndex to false.

const schema = new Schema({ /* ... */ }, { autoIndex: false });
const Clock = mongoose.model('Clock', schema);
Clock.ensureIndexes(callback);

The autoIndex option is set to true by default. You can change this
default by setting mongoose.set('autoIndex', false);

option: autoCreate

Before Mongoose builds indexes, it calls Model.createCollection() to create the underlying collection in MongoDB by default.
Calling createCollection() sets the collection’s default collation based on the collation option and establishes the collection as
a capped collection if you set the capped schema option.

You can disable this behavior by setting autoCreate to false using mongoose.set('autoCreate', false).
Like autoIndex, autoCreate is helpful for development and test environments, but you may want to disable it for production to avoid unnecessary database calls.

Unfortunately, createCollection() cannot change an existing collection.
For example, if you add capped: { size: 1024 } to your schema and the existing collection is not capped, createCollection() will not overwrite the existing collection.
That is because the MongoDB server does not allow changing a collection’s options without dropping the collection first.

const schema = new Schema({ name: String }, {
  autoCreate: false,
  capped: { size: 1024 }
});
const Test = mongoose.model('Test', schema);

// No-op if collection already exists, even if the collection is not capped.
// This means that `capped` won't be applied if the 'tests' collection already exists.
await Test.createCollection();

option: bufferCommands

By default, mongoose buffers commands when the connection goes down until
the driver manages to reconnect. To disable buffering, set bufferCommands
to false.

const schema = new Schema({ /* ... */ }, { bufferCommands: false });

The schema bufferCommands option overrides the global bufferCommands option.

mongoose.set('bufferCommands', true);
// Schema option below overrides the above, if the schema option is set.
const schema = new Schema({ /* ... */ }, { bufferCommands: false });

option: bufferTimeoutMS

If bufferCommands is on, this option sets the maximum amount of time Mongoose buffering will wait before
throwing an error. If not specified, Mongoose will use 10000 (10 seconds).

// If an operation is buffered for more than 1 second, throw an error.
const schema = new Schema({ /* ... */ }, { bufferTimeoutMS: 1000 });

option: capped

Mongoose supports MongoDBs capped
collections. To specify the underlying MongoDB collection be capped, set
the capped option to the maximum size of the collection in
bytes.

new Schema({ /* ... */ }, { capped: 1024 });

The capped option may also be set to an object if you want to pass
additional options like max.
In this case you must explicitly pass the size option, which is required.

new Schema({ /* ... */ }, { capped: { size: 1024, max: 1000, autoIndexId: true } });

option: collection

Mongoose by default produces a collection name by passing the model name to
the utils.toCollectionName method.
This method pluralizes the name. Set this option if you need a different name
for your collection.

const dataSchema = new Schema({ /* ... */ }, { collection: 'data' });

option: discriminatorKey

When you define a discriminator, Mongoose adds a path to your
schema that stores which discriminator a document is an instance of. By default, Mongoose
adds an __t path, but you can set discriminatorKey to overwrite this default.

const baseSchema = new Schema({}, { discriminatorKey: 'type' });
const BaseModel = mongoose.model('Test', baseSchema);

const personSchema = new Schema({ name: String });
const PersonModel = BaseModel.discriminator('Person', personSchema);

const doc = new PersonModel({ name: 'James T. Kirk' });
// Without `discriminatorKey`, Mongoose would store the discriminator
// key in `__t` instead of `type`
doc.type; // 'Person'

option: id

Mongoose assigns each of your schemas an id virtual getter by default
which returns the document’s _id field cast to a string, or in the case of
ObjectIds, its hexString. If you don’t want an id getter added to your
schema, you may disable it by passing this option at schema construction time.

// default behavior
const schema = new Schema({ name: String });
const Page = mongoose.model('Page', schema);
const p = new Page({ name: 'mongodb.org' });
console.log(p.id); // '50341373e894ad16347efe01'

// disabled id
const schema = new Schema({ name: String }, { id: false });
const Page = mongoose.model('Page', schema);
const p = new Page({ name: 'mongodb.org' });
console.log(p.id); // undefined

option: _id

Mongoose assigns each of your schemas an _id field by default if one
is not passed into the Schema constructor.
The type assigned is an ObjectId
to coincide with MongoDB’s default behavior. If you don’t want an _id
added to your schema at all, you may disable it using this option.

You can only use this option on subdocuments. Mongoose can’t
save a document without knowing its id, so you will get an error if
you try to save a document without an _id.

// default behavior
const schema = new Schema({ name: String });
const Page = mongoose.model('Page', schema);
const p = new Page({ name: 'mongodb.org' });
console.log(p); // { _id: '50341373e894ad16347efe01', name: 'mongodb.org' }

// disabled _id
const childSchema = new Schema({ name: String }, { _id: false });
const parentSchema = new Schema({ children: [childSchema] });

const Model = mongoose.model('Model', parentSchema);

Model.create({ children: [{ name: 'Luke' }] }, (error, doc) => {
  // doc.children[0]._id will be undefined
});

option: minimize

Mongoose will, by default, «minimize» schemas by removing empty objects.

const schema = new Schema({ name: String, inventory: {} });
const Character = mongoose.model('Character', schema);

// will store `inventory` field if it is not empty
const frodo = new Character({ name: 'Frodo', inventory: { ringOfPower: 1 } });
await frodo.save();
let doc = await Character.findOne({ name: 'Frodo' }).lean();
doc.inventory; // { ringOfPower: 1 }

// will not store `inventory` field if it is empty
const sam = new Character({ name: 'Sam', inventory: {} });
await sam.save();
doc = await Character.findOne({ name: 'Sam' }).lean();
doc.inventory; // undefined

This behavior can be overridden by setting minimize option to false. It
will then store empty objects.

const schema = new Schema({ name: String, inventory: {} }, { minimize: false });
const Character = mongoose.model('Character', schema);

// will store `inventory` if empty
const sam = new Character({ name: 'Sam', inventory: {} });
await sam.save();
doc = await Character.findOne({ name: 'Sam' }).lean();
doc.inventory; // {}

To check whether an object is empty, you can use the $isEmpty() helper:

const sam = new Character({ name: 'Sam', inventory: {} });
sam.$isEmpty('inventory'); // true

sam.inventory.barrowBlade = 1;
sam.$isEmpty('inventory'); // false

option: read

Allows setting query#read options at the
schema level, providing us a way to apply default
ReadPreferences
to all queries derived from a model.

const schema = new Schema({ /* ... */ }, { read: 'primary' });            // also aliased as 'p'
const schema = new Schema({ /* ... */ }, { read: 'primaryPreferred' });   // aliased as 'pp'
const schema = new Schema({ /* ... */ }, { read: 'secondary' });          // aliased as 's'
const schema = new Schema({ /* ... */ }, { read: 'secondaryPreferred' }); // aliased as 'sp'
const schema = new Schema({ /* ... */ }, { read: 'nearest' });            // aliased as 'n'

The alias of each pref is also permitted so instead of having to type out
‘secondaryPreferred’ and getting the spelling wrong, we can simply pass ‘sp’.

The read option also allows us to specify tag sets. These tell the
driver from which members
of the replica-set it should attempt to read. Read more about tag sets
here and
here.

NOTE: you may also specify the driver read preference strategy
option when connecting:

// pings the replset members periodically to track network latency
const options = { replset: { strategy: 'ping' } };
mongoose.connect(uri, options);

const schema = new Schema({ /* ... */ }, { read: ['nearest', { disk: 'ssd' }] });
mongoose.model('JellyBean', schema);

option: writeConcern

Allows setting write concern
at the schema level.

const schema = new Schema({ name: String }, {
  writeConcern: {
    w: 'majority',
    j: true,
    wtimeout: 1000
  }
});

option: shardKey

The shardKey option is used when we have a sharded MongoDB architecture.
Each sharded collection is given a shard key which must be present in all
insert/update operations. We just need to set this schema option to the same
shard key and we’ll be all set.

new Schema({ /* ... */ }, { shardKey: { tag: 1, name: 1 } });

Note that Mongoose does not send the shardcollection command for you. You
must configure your shards yourself.

option: strict

The strict option, (enabled by default), ensures that values passed to our
model constructor that were not specified in our schema do not get saved to
the db.

const thingSchema = new Schema({ /* ... */ })
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing({ iAmNotInTheSchema: true });
thing.save(); // iAmNotInTheSchema is not saved to the db

// set to false..
const thingSchema = new Schema({ /* ... */ }, { strict: false });
const thing = new Thing({ iAmNotInTheSchema: true });
thing.save(); // iAmNotInTheSchema is now saved to the db!!

This also affects the use of doc.set() to set a property value.

const thingSchema = new Schema({ /* ... */ });
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing;
thing.set('iAmNotInTheSchema', true);
thing.save(); // iAmNotInTheSchema is not saved to the db

This value can be overridden at the model instance level by passing a second
boolean argument:

const Thing = mongoose.model('Thing');
const thing = new Thing(doc, true);  // enables strict mode
const thing = new Thing(doc, false); // disables strict mode

The strict option may also be set to "throw" which will cause errors
to be produced instead of dropping the bad data.

NOTE: Any key/val set on the instance that does not exist in your schema
is always ignored, regardless of schema option.

const thingSchema = new Schema({ /* ... */ });
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing;
thing.iAmNotInTheSchema = true;
thing.save(); // iAmNotInTheSchema is never saved to the db

option: strictQuery

Mongoose supports a separate strictQuery option to avoid strict mode for query filters.
This is because empty query filters cause Mongoose to return all documents in the model, which can cause issues.

const mySchema = new Schema({ field: Number }, { strict: true });
const MyModel = mongoose.model('Test', mySchema);
// Mongoose will filter out `notInSchema: 1` because `strict: true`, meaning this query will return
// _all_ documents in the 'tests' collection
MyModel.find({ notInSchema: 1 });

The strict option does apply to updates.
The strictQuery option is just for query filters.

// Mongoose will strip out `notInSchema` from the update if `strict` is
// not `false`
MyModel.updateMany({}, { $set: { notInSchema: 1 } });

Mongoose has a separate strictQuery option to toggle strict mode for the filter parameter to queries.

const mySchema = new Schema({ field: Number }, {
  strict: true,
  strictQuery: false // Turn off strict mode for query filters
});
const MyModel = mongoose.model('Test', mySchema);
// Mongoose will not strip out `notInSchema: 1` because `strictQuery` is false
MyModel.find({ notInSchema: 1 });

In general, we do not recommend passing user-defined objects as query filters:

// Don't do this!
const docs = await MyModel.find(req.query);

// Do this instead:
const docs = await MyModel.find({ name: req.query.name, age: req.query.age }).setOptions({ sanitizeFilter: true });

In Mongoose 7, strictQuery is false by default.
However, you can override this behavior globally:

// Set `strictQuery` to `true` to omit unknown fields in queries.
mongoose.set('strictQuery', true);

option: toJSON

Exactly the same as the toObject option but only applies when
the document’s toJSON method is called.

const schema = new Schema({ name: String });
schema.path('name').get(function(v) {
  return v + ' is my name';
});
schema.set('toJSON', { getters: true, virtuals: false });
const M = mongoose.model('Person', schema);
const m = new M({ name: 'Max Headroom' });
console.log(m.toObject()); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom' }
console.log(m.toJSON()); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my name' }
// since we know toJSON is called whenever a js object is stringified:
console.log(JSON.stringify(m)); // { "_id": "504e0cd7dd992d9be2f20b6f", "name": "Max Headroom is my name" }

To see all available toJSON/toObject options, read this.

option: toObject

Documents have a toObject method
which converts the mongoose document into a plain JavaScript object. This
method accepts a few options. Instead of applying these options on a
per-document basis, we may declare the options at the schema level and have
them applied to all of the schema’s documents by default.

To have all virtuals show up in your console.log output, set the
toObject option to { getters: true }:

const schema = new Schema({ name: String });
schema.path('name').get(function(v) {
  return v + ' is my name';
});
schema.set('toObject', { getters: true });
const M = mongoose.model('Person', schema);
const m = new M({ name: 'Max Headroom' });
console.log(m); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my name' }

To see all available toObject options, read this.

option: typeKey

By default, if you have an object with key ‘type’ in your schema, mongoose
will interpret it as a type declaration.

// Mongoose interprets this as 'loc is a String'
const schema = new Schema({ loc: { type: String, coordinates: [Number] } });

However, for applications like geoJSON,
the ‘type’ property is important. If you want to control which key mongoose
uses to find type declarations, set the ‘typeKey’ schema option.

const schema = new Schema({
  // Mongoose interprets this as 'loc is an object with 2 keys, type and coordinates'
  loc: { type: String, coordinates: [Number] },
  // Mongoose interprets this as 'name is a String'
  name: { $type: String }
}, { typeKey: '$type' }); // A '$type' key means this object is a type declaration

option: validateBeforeSave

By default, documents are automatically validated before they are saved to
the database. This is to prevent saving an invalid document. If you want to
handle validation manually, and be able to save objects which don’t pass
validation, you can set validateBeforeSave to false.

const schema = new Schema({ name: String });
schema.set('validateBeforeSave', false);
schema.path('name').validate(function(value) {
  return value != null;
});
const M = mongoose.model('Person', schema);
const m = new M({ name: null });
m.validate(function(err) {
  console.log(err); // Will tell you that null is not allowed.
});
m.save(); // Succeeds despite being invalid

option: versionKey

The versionKey is a property set on each document when first created by
Mongoose. This keys value contains the internal
revision
of the document. The versionKey option is a string that represents the
path to use for versioning. The default is __v. If this conflicts with
your application you can configure as such:

const schema = new Schema({ name: 'string' });
const Thing = mongoose.model('Thing', schema);
const thing = new Thing({ name: 'mongoose v3' });
await thing.save(); // { __v: 0, name: 'mongoose v3' }

// customized versionKey
new Schema({ /* ... */ }, { versionKey: '_somethingElse' })
const Thing = mongoose.model('Thing', schema);
const thing = new Thing({ name: 'mongoose v3' });
thing.save(); // { _somethingElse: 0, name: 'mongoose v3' }

Note that Mongoose’s default versioning is not a full optimistic concurrency
solution. Mongoose’s default versioning only operates on arrays as shown below.

// 2 copies of the same document
const doc1 = await Model.findOne({ _id });
const doc2 = await Model.findOne({ _id });

// Delete first 3 comments from `doc1`
doc1.comments.splice(0, 3);
await doc1.save();

// The below `save()` will throw a VersionError, because you're trying to
// modify the comment at index 1, and the above `splice()` removed that
// comment.
doc2.set('comments.1.body', 'new comment');
await doc2.save();

If you need optimistic concurrency support for save(), you can set the optimisticConcurrency option

Document versioning can also be disabled by setting the versionKey to
false.
DO NOT disable versioning unless you know what you are doing.

new Schema({ /* ... */ }, { versionKey: false });
const Thing = mongoose.model('Thing', schema);
const thing = new Thing({ name: 'no versioning please' });
thing.save(); // { name: 'no versioning please' }

Mongoose only updates the version key when you use save().
If you use update(), findOneAndUpdate(), etc. Mongoose will not
update the version key. As a workaround, you can use the below middleware.

schema.pre('findOneAndUpdate', function() {
  const update = this.getUpdate();
  if (update.__v != null) {
    delete update.__v;
  }
  const keys = ['$set', '$setOnInsert'];
  for (const key of keys) {
    if (update[key] != null && update[key].__v != null) {
      delete update[key].__v;
      if (Object.keys(update[key]).length === 0) {
        delete update[key];
      }
    }
  }
  update.$inc = update.$inc || {};
  update.$inc.__v = 1;
});

option: optimisticConcurrency

Optimistic concurrency is a strategy to ensure
the document you’re updating didn’t change between when you loaded it using find() or findOne(), and when
you update it using save().

For example, suppose you have a House model that contains a list of photos, and a status that represents
whether this house shows up in searches. Suppose that a house that has status 'APPROVED' must have at least
two photos. You might implement the logic of approving a house document as shown below:

async function markApproved(id) {
  const house = await House.findOne({ _id });
  if (house.photos.length < 2) {
    throw new Error('House must have at least two photos!');
  }

  house.status = 'APPROVED';
  await house.save();
}

The markApproved() function looks right in isolation, but there might be a potential issue: what if another
function removes the house’s photos between the findOne() call and the save() call? For example, the below
code will succeed:

const house = await House.findOne({ _id });
if (house.photos.length < 2) {
  throw new Error('House must have at least two photos!');
}

const house2 = await House.findOne({ _id });
house2.photos = [];
await house2.save();

// Marks the house as 'APPROVED' even though it has 0 photos!
house.status = 'APPROVED';
await house.save();

If you set the optimisticConcurrency option on the House model’s schema, the above script will throw an
error.

const House = mongoose.model('House', Schema({
  status: String,
  photos: [String]
}, { optimisticConcurrency: true }));

const house = await House.findOne({ _id });
if (house.photos.length < 2) {
  throw new Error('House must have at least two photos!');
}

const house2 = await House.findOne({ _id });
house2.photos = [];
await house2.save();

// Throws 'VersionError: No matching document found for id "..." version 0'
house.status = 'APPROVED';
await house.save();

option: collation

Sets a default collation
for every query and aggregation. Here’s a beginner-friendly overview of collations.

const schema = new Schema({
  name: String
}, { collation: { locale: 'en_US', strength: 1 } });

const MyModel = db.model('MyModel', schema);

MyModel.create([{ name: 'val' }, { name: 'Val' }]).
  then(() => {
    return MyModel.find({ name: 'val' });
  }).
  then((docs) => {
    // `docs` will contain both docs, because `strength: 1` means
    // MongoDB will ignore case when matching.
  });

option: timeseries

If you set the timeseries option on a schema, Mongoose will create a timeseries collection for any model that you create from that schema.

const schema = Schema({ name: String, timestamp: Date, metadata: Object }, {
  timeseries: {
    timeField: 'timestamp',
    metaField: 'metadata',
    granularity: 'hours'
  },
  autoCreate: false,
  expireAfterSeconds: 86400
});

// `Test` collection will be a timeseries collection
const Test = db.model('Test', schema);

option: skipVersioning

skipVersioning allows excluding paths from versioning (i.e., the internal
revision will not be incremented even if these paths are updated). DO NOT
do this unless you know what you’re doing. For subdocuments, include this
on the parent document using the fully qualified path.

new Schema({ /* ... */ }, { skipVersioning: { dontVersionMe: true } });
thing.dontVersionMe.push('hey');
thing.save(); // version is not incremented

option: timestamps

The timestamps option tells Mongoose to assign createdAt and updatedAt fields
to your schema. The type assigned is Date.

By default, the names of the fields are createdAt and updatedAt. Customize
the field names by setting timestamps.createdAt and timestamps.updatedAt.

The way timestamps works under the hood is:

  • If you create a new document, mongoose simply sets createdAt, and updatedAt to the time of creation.
  • If you update a document, mongoose will add updatedAt to the $set object.
  • If you set upsert: true on an update operation, mongoose will use $setOnInsert operator to add createdAt to the document in case the upsert operation resulted into a new inserted document.
const thingSchema = new Schema({ /* ... */ }, { timestamps: { createdAt: 'created_at' } });
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing();
await thing.save(); // `created_at` & `updatedAt` will be included

// With updates, Mongoose will add `updatedAt` to `$set`
await Thing.updateOne({}, { $set: { name: 'Test' } });

// If you set upsert: true, Mongoose will add `created_at` to `$setOnInsert` as well
await Thing.findOneAndUpdate({}, { $set: { name: 'Test2' } });

// Mongoose also adds timestamps to bulkWrite() operations
// See https://mongoosejs.com/docs/api/model.html#model_Model-bulkWrite
await Thing.bulkWrite([
  {
    insertOne: {
      document: {
        name: 'Jean-Luc Picard',
        ship: 'USS Stargazer'
      // Mongoose will add `created_at` and `updatedAt`
      }
    }
  },
  {
    updateOne: {
      filter: { name: 'Jean-Luc Picard' },
      update: {
        $set: {
          ship: 'USS Enterprise'
        // Mongoose will add `updatedAt`
        }
      }
    }
  }
]);

By default, Mongoose uses new Date() to get the current time.
If you want to overwrite the function
Mongoose uses to get the current time, you can set the
timestamps.currentTime option. Mongoose will call the
timestamps.currentTime function whenever it needs to get
the current time.

const schema = Schema({
  createdAt: Number,
  updatedAt: Number,
  name: String
}, {
  // Make Mongoose use Unix time (seconds since Jan 1, 1970)
  timestamps: { currentTime: () => Math.floor(Date.now() / 1000) }
});

option: pluginTags

Mongoose supports defining global plugins, plugins that apply to all schemas.

// Add a `meta` property to all schemas
mongoose.plugin(function myPlugin(schema) {
  schema.add({ meta: {} });
});

Sometimes, you may only want to apply a given plugin to some schemas.
In that case, you can add pluginTags to a schema:

const schema1 = new Schema({
  name: String
}, { pluginTags: ['useMetaPlugin'] });

const schema2 = new Schema({
  name: String
});

If you call plugin() with a tags option, Mongoose will only apply that plugin to schemas that have a matching entry in pluginTags.

// Add a `meta` property to all schemas
mongoose.plugin(function myPlugin(schema) {
  schema.add({ meta: {} });
}, { tags: ['useMetaPlugin'] });

option: selectPopulatedPaths

By default, Mongoose will automatically select() any populated paths for
you, unless you explicitly exclude them.

const bookSchema = new Schema({
  title: 'String',
  author: { type: 'ObjectId', ref: 'Person' }
});
const Book = mongoose.model('Book', bookSchema);

// By default, Mongoose will add `author` to the below `select()`.
await Book.find().select('title').populate('author');

// In other words, the below query is equivalent to the above
await Book.find().select('title author').populate('author');

To opt out of selecting populated fields by default, set selectPopulatedPaths
to false in your schema.

const bookSchema = new Schema({
  title: 'String',
  author: { type: 'ObjectId', ref: 'Person' }
}, { selectPopulatedPaths: false });
const Book = mongoose.model('Book', bookSchema);

// Because `selectPopulatedPaths` is false, the below doc will **not**
// contain an `author` property.
const doc = await Book.findOne().select('title').populate('author');

option: storeSubdocValidationError

For legacy reasons, when there is a validation error in subpath of a
single nested schema, Mongoose will record that there was a validation error
in the single nested schema path as well. For example:

const childSchema = new Schema({ name: { type: String, required: true } });
const parentSchema = new Schema({ child: childSchema });

const Parent = mongoose.model('Parent', parentSchema);

// Will contain an error for both 'child.name' _and_ 'child'
new Parent({ child: {} }).validateSync().errors;

Set the storeSubdocValidationError to false on the child schema to make
Mongoose only reports the parent error.

const childSchema = new Schema({
  name: { type: String, required: true }
}, { storeSubdocValidationError: false }); // <-- set on the child schema
const parentSchema = new Schema({ child: childSchema });

const Parent = mongoose.model('Parent', parentSchema);

// Will only contain an error for 'child.name'
new Parent({ child: {} }).validateSync().errors;

With ES6 Classes

Schemas have a loadClass() method
that you can use to create a Mongoose schema from an ES6 class:

  • ES6 class methods become Mongoose methods
  • ES6 class statics become Mongoose statics
  • ES6 getters and setters become Mongoose virtuals

Here’s an example of using loadClass() to create a schema from an ES6 class:

class MyClass {
  myMethod() { return 42; }
  static myStatic() { return 42; }
  get myVirtual() { return 42; }
}

const schema = new mongoose.Schema();
schema.loadClass(MyClass);

console.log(schema.methods); // { myMethod: [Function: myMethod] }
console.log(schema.statics); // { myStatic: [Function: myStatic] }
console.log(schema.virtuals); // { myVirtual: VirtualType { ... } }

Pluggable

Schemas are also pluggable which allows us to package up reusable features into
plugins that can be shared with the community or just between your projects.

Further Reading

Here’s an alternative introduction to Mongoose schemas.

To get the most out of MongoDB, you need to learn the basics of MongoDB schema design.
SQL schema design (third normal form) was designed to minimize storage costs,
whereas MongoDB schema design is about making common queries as fast as possible.
The 6 Rules of Thumb for MongoDB Schema Design blog series
is an excellent resource for learning the basic rules for making your queries
fast.

Users looking to master MongoDB schema design in Node.js should look into
The Little MongoDB Schema Design Book
by Christian Kvalheim, the original author of the MongoDB Node.js driver.
This book shows you how to implement performant schemas for a laundry list
of use cases, including e-commerce, wikis, and appointment bookings.

Next Up

Now that we’ve covered Schemas, let’s take a look at SchemaTypes.

Создано: 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

Понравилась статья? Поделить с друзьями:
  • Ларипронт инструкция по применению детям со скольки лет можно
  • Comfeel cleanser инструкция по применению на русском языке
  • Daeng gi meo ri средство против выпадения инструкция
  • Триазавирин инструкция по применению таблетки взрослым при ковиде отзывы пациентов
  • Долобене мазь для чего применяется инструкция по применению взрослым