DevGang
Авторизоваться

Mongoose с Node.js - моделирование объектных данных 

NoSQL привнес гибкость в табличный мир баз данных. В частности, MongoDB стал отличным вариантом для хранения неструктурированных документов JSON. Данные начинаются как JSON в пользовательском интерфейсе и претерпевают очень мало преобразований для сохранения, поэтому мы получаем преимущества от повышения производительности и сокращения времени обработки.

Но NoSQL не означает полного отсутствия структуры. Нам все еще нужно проверить и преобразовать наши данные перед их сохранением, и нам все еще может потребоваться применить к ним некоторую бизнес-логику. Это место, которое заполняет Mongoose.

В этой статье мы узнаем на примере приложения, как мы можем использовать Mongoose для моделирования наших данных и проверки их перед сохранением в MongoDB.

Мы напишем модель для приложения «Генеалогия» - человека с несколькими личными свойствами, включая их родителей. Мы также увидим, как мы можем использовать эту модель для создания и изменения лиц и сохранения их в MongoDB.

Что такое Mongoose?

Как работает MongoDB

Чтобы понять, что такое Mongoose, нам сначала нужно понять в общих чертах, как работает MongoDB. Базовая единица данных, которую мы можем сохранить в MongoDB, - это документ. Несмотря на то, что они хранятся как двоичные, когда мы запрашиваем базу данных, мы получаем ее представление в виде объекта JSON.

Связанные документы могут храниться в коллекциях, подобных таблицам в реляционных базах данных. На этом аналогия заканчивается, поскольку мы определяем, что считать «связанными документами».

MongoDB не навязывает структуру документов. Например, мы можем сохранить этот документ в коллекцию Person:

{
  "name": "Alice"
}

А затем в той же коллекции мы могли бы сохранить, казалось бы, несвязанный документ без общих свойств или структуры:

{
  "latitude": 53.3498,
  "longitude": 6.2603
}

В этом заключается новизна баз данных NoSQL. Мы придаем смысл нашим данным и храним их так, как нам кажется лучше. База данных не налагает никаких ограничений.

Цель Mongoose

Хотя MongoDB не навязывает структуру, приложения обычно управляют данными с ее помощью. Мы получаем данные, и нам необходимо их проверить, чтобы убедиться, что мы получили именно то, что нам нужно. Нам также может потребоваться обработать данные каким-либо образом перед их сохранением. Здесь и вступает в игру Mongoose.

Mongoose - это NPM пакет для приложений NodeJS. Он позволяет определять схемы для наших данных, а также абстрагироваться от доступа к MongoDB. Таким образом мы можем гарантировать, что все сохраненные документы имеют общую структуру и содержат необходимые свойства.

Давайте теперь посмотрим, как определить схему.

Установка Mongoose и создание схемы Person

Давайте запустим проект Node со свойствами по умолчанию и схемой Person:

npm init -y

После инициализации проекта давайте продолжим и установим mongoose, используя npm:

npm install --save mongoose

mongoose также автоматически установит NPM модуль mongodb. Вы не будете использовать его напрямую. Этим займется Mongoose.

Чтобы работать с Mongoose, нам нужно импортировать его в наши скрипты:

let mongoose = require('mongoose');

А затем подключитесь к базе данных с помощью:

mongoose.connect('mongodb://localhost:27017/genealogy', {useNewUrlParser: true, useUnifiedTopology: true});

Поскольку база данных еще не существует, она будет создана. Мы будем использовать последний инструмент для анализа строки подключения, установив значение useNewUrlParser как true а также будем использовать последнюю версию драйвера MongoDB с useUnifiedTopology равным true.

mongoose.connect() предполагает, что сервер MongoDB работает локально на порту по умолчанию и без учетных данных. Один из простых способов заставить MongoDB работать таким образом - это Docker:

docker run -p 27017:27017 mongo

Созданного контейнера нам будет достаточно, чтобы попробовать Mongoose, хотя данные, сохраненные в MongoDB, не будут постоянными.

Схема и модель Person

После предыдущих необходимых объяснений мы можем сосредоточиться на написании нашей схемы человека и компиляции из нее модели.

Схема в Mongoose сопоставляется с коллекцией MongoDB и определяет формат для всех документов в этой коллекции. Всем свойствам внутри схемы должен быть назначен SchemaType. Например, имя нашего Person можно определить так:

const PersonSchema = new mongoose.Schema({
    name:  { type: String},
});

Или еще проще, вот так:

const PersonSchema = new mongoose.Schema({
    name: String,
});

String является одним из нескольких SchemaTypes, определенных Mongoose. Остальное можно найти в документации Mongoose.

Ссылка на другие схемы

Мы можем ожидать, что все приложения среднего размера будут иметь более одной схемы, и, возможно, эти схемы будут каким-то образом связаны.

В нашем примере, чтобы представить генеалогическое древо, нам нужно добавить в нашу схему два атрибута:

const PersonSchema = new mongoose.Schema({
    // ...
    mother: { type: mongoose.Schema.Types.ObjectId, ref: 'Person' },
    father: { type: mongoose.Schema.Types.ObjectId, ref: 'Person' },
});

Человек может иметь mother и father. В Mongoose это можно представить, сохранив идентификатор документа, на который есть ссылка mongoose.Schema.Types.ObjectId, а не самого объекта.

ref должно быть название модели, на  которую мы ссылаемся. Позже мы узнаем больше о моделях, но пока достаточно знать, что схема относится только к одной модели и Person является моделью PersonSchema.

Наш случай немного особенный, потому что оба mother и father также будут содержать людей, но способ определения этих отношений одинаков во всех случаях.

Встроенная проверка

Все SchemaType идут со встроенной проверкой по умолчанию. В зависимости от выбранного мы можем определить лимиты и другие требования SchemaType. Для того, чтобы увидеть некоторые примеры, давайте добавим surname, yearBorn и notes к нашему Person:

const PersonSchema = new mongoose.Schema({
    name: { type: String, index: true, required: true },
    surname: { type: String, index: true },
    yearBorn: { type: Number, min: -5000, max: (new Date).getFullYear() },
    notes: { type: String, minlength: 5 },
});

Все встроенные SchemaType могут быть required. В нашем случае мы хотим, чтобы у всех людей было хотя бы имя. Тип Number позволяет устанавливать мин и максимальными значениями.

Свойство index заставит Mongoose создать индекс в базе данных. Это облегчает эффективное выполнение запросов. Выше мы определили индексы name и surname. Мы всегда будем искать людей по именам.

Пользовательская проверка

Встроенные SchemaType позволяют настраивать собственные проверки. Это особенно полезно, когда у нас есть свойство, которое может содержать только определенные значения. Добавим свойство photosURLs в наш массив URL-адресов их фотографий Person:

const PersonSchema = new mongoose.Schema({
    // ...
    photosURLs: [
      {
        type: String,
        validate: {
          validator: function(value) {
            const urlPattern = /(http|https):\/\/(\w+:{0,1}\w*#)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%#!\-/]))?/;
            const urlRegExp = new RegExp(urlPattern);
            return value.match(urlRegExp);
          },
          message: props => `${props.value} is not a valid URL`
        }
      }
    ],
});

photosURLs это просто массив строк photosURLs: [String]. Что делает это свойство особенным, так это то, что нам нужна настраиваемая проверка, чтобы подтвердить, что добавленные значения имеют формат URL-адреса.

В приведенной выше функции validator() используется регулярное выражение, которое соответствует типичным URL-адресам в Интернете, которые должны начинаться с http(s)://.

Если нам нужен более сложный SchemaType, мы можем создать свой собственный, но нам стоит поискать, возможно он уже существует.

Например, пакет mongoose-type-url, который мы могли бы использовать вместо нашей валидации.

Виртуальные свойства

Виртуальные объекты - это свойства документа, которые не сохраняются в базе данных. Они результат расчета. В нашем примере было бы полезно задать полное имя человека в одной строке, а не разделять name и surname.

Посмотрим, как этого добиться после определения нашей первоначальной схемы:

PersonSchema.virtual('fullName').
    get(function() { 
      if(this.surname)
        return this.name + ' ' + this.surname; 
      return this.name;
    }).
    set(function(fullName) {
      fullName = fullName.split(' ');
      this.name = fullName[0];
      this.surname = fullName[1];
    });

Приведенное выше виртуальное свойство fullName делает некоторые предположения для простоты: у каждого человека есть как минимум имя и фамилия. Мы столкнемся с проблемами, если у человека есть отчество, составное имя или фамилия. Все эти ограничения могут быть закреплены внутри get() и set() функции, определенные выше.

Поскольку виртуальные данные не сохраняются в базе данных, мы не можем использовать их в качестве фильтра при поиске людей в базе данных. В нашем случае нам нужно будет использовать name и surname.

ПО промежуточного слоя

Промежуточное ПО - это функции или перехватчики, которые могут выполняться до или после стандартных методов Mongoose, например, таких как save() или find().

Человек может иметь mother и father. Как мы уже говорили ранее, мы сохраняем эти отношения, сохраняя идентификатор объекта как свойства человека, а не сами объекты. Было бы неплохо заполнить оба свойства самими объектами, а не только идентификаторами.

Это может быть достигнуто как функция pre(), связанная с методом Mongoose findOne():

PersonSchema.pre('findOne', function(next) {
    this.populate('mother').populate('father');
    next();
});

Вышеупомянутой функции необходимо вызвать функцию next(), полученную в качестве параметра, чтобы продолжить обработку других хуков.

populate() - это метод Mongoose для замены идентификаторов объектами, которые они представляют, и мы используем его для получения родителей при поиске только одного человека.

Мы могли бы добавить этот хук к другим функциям поиска, например find(). Мы могли бы даже найти родителей рекурсивно, если бы захотели. Но мы должны обращаться с populate() осторожно, так как каждый вызов - это выборка из базы данных.

Создайте модель для схемы

Чтобы начать создавать документы на основе нашей схемы Person, последний шаг - это скомпилировать модель на основе схемы:

const Person = mongoose.model('Person', PersonSchema);

Первым аргументом будет единственное имя коллекции, о которой мы говорим. Это значение ref, которое мы придали свойствам mother и father. Второй аргумент - это Schema, то что мы определили ранее.

Метод model() делает копию всего что мы определили в схеме. Он также содержит все методы Mongoose, которые мы будем использовать для взаимодействия с базой данных.

Модель - это единственное, что нам теперь нужно. Мы могли бы даже использовать module.exports, чтобы сделать Person доступным в других модулях нашего приложения:

module.exports.Person = mongoose.model('Person', PersonSchema);
module.exports.db = mongoose;

Мы также экспортировали модуль mongoose. Он понадобится нам для отключения от базы данных до завершения работы приложения.

Мы можем импортировать модуль таким образом:

const {db, Person} = require('./persistence');

Как использовать модель

Модель, которую мы скомпилировали в последнем разделе, содержит все, что нам нужно для взаимодействия с коллекцией в базе данных.

Давайте теперь посмотрим, как мы будем использовать нашу модель для всех операций CRUD.

Создание Person

Мы можем создать Person, просто выполнив:

let alice = new Person({name: 'Alice'});

name это единственное обязательное свойство. Давайте создадим другого человека, но на этот раз с использованием виртуального свойства:

let bob = new Person({fullName: 'Bob Brown'});

Теперь, когда у нас есть первые два человека, мы можем создать нового со всеми заполненными свойствами, включая родителей:

let charles = new Person({
  fullName: 'Charles Brown',
  photosURLs: ['https://bit.ly/34Kvbsh'],
  yearBorn: 1922,
  notes: 'Famous blues singer and pianist. Parents not real.',
  mother: alice._id,
  father: bob._id,
});

Все значения для этого последнего человека установлены на допустимые, так как проверка выдаст ошибку, как только эта строка будет выполнена. Например, если бы мы установили URL-адрес первой фотографии не на ссылку, мы бы получили ошибку:

ValidationError: Person validation failed: photosURLs.0: wrong_url is not a valid URL

Как объяснялось ранее, родители были заполнены идентификаторами первых двух человек, а не объектами.

Мы создали трех человек, но они еще не сохранены в базе данных. Сделаем это дальше:

alice.save();
bob.save();

Операции с базой данных асинхронны. Если мы хотим дождаться завершения, мы можем использовать async / await:

await charles.save();

Теперь, когда все люди будут сохранены в базе данных, мы можем получить их обратно с методами find() и findOne().

Получить одно или несколько Person

Все методы поиска в Mongoose требуют аргумента для фильтрации поиска. Вернемся к последнему созданному нами человеку:

let dbCharles = await Person.findOne({name: 'Charles', surname: 'Brown'}).exec();

findOne() возвращает запрос, поэтому для получения результата нам нужно выполнить его с помощью exec(), а затем дождаться результата с помощью await.

Поскольку мы прикрепили к методу findOne() ловушку для заполнения родителей человека, теперь мы можем обращаться к ним напрямую:

console.log(dbCharles.mother.fullName);

В нашем случае мы знаем, что запрос вернет только один результат, но даже если более одного человека соответствует фильтру, будет возвращен только первый результат.

Мы можем получить более одного результата, если воспользуемся методом find():

let all = await Person.find({}).exec();

Мы вернем массив, который сможем перебрать.

Обновление Person

Если у нас уже есть Person, так как мы только что его создали или извлекли, мы можем обновить и сохранить изменения, выполнив следующие действия:

alice.surname = 'Adams';
charles.photosURLs.push('https://bit.ly/2QJCnMV');
await alice.save();
await charles.save();

Поскольку оба человека уже существуют в базе данных, Mongoose отправит команду обновления только с измененными полями, а не со всем документом.

Удалить Person

Как и поиск, удаление может быть выполнено для одного или нескольких Person:

await Person.deleteOne({name: 'Alice'});
await Person.deleteMany({}).exec();

После выполнения этих двух команд коллекция будет пустой.

Вывод

В этой статье мы увидели, как Mongoose может быть очень полезен в наших проектах NodeJS и MongoDB.

В большинстве проектов с MongoDB нам необходимо хранить данные в определенном формате. Приятно знать, что Mongoose предоставляет простой способ моделирования и проверки этих данных.

Источник:

#JavaScript #MongoDB #NodeJS
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

Оплатив хостинг 25$ в подарок вы получите 100$ на счет

Получить