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

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

Вступление

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

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

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

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

Что такое мангуст?

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

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

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

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

 { 
 "name": "Alice" 
 } 

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

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

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

Цель мангуста

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

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

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

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

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

 $ npm init -y 

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

 $ npm install --save mongoose 

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

Чтобы работать с 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, не будут постоянными.

Схема и модель человека

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

Схема в 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 . Чтобы увидеть несколько примеров, давайте добавим к нашему Person surname , год yearBorn и notes :

 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 }, 
 }); 

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

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

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

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

 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 добавляет настраиваемый SchemaType который мы могли бы использовать, mongoose.SchemaTypes.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 father и mother нашего человека. Второй аргумент - это Schema мы определили ранее.

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

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

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

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

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

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

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

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

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

Создать людей

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

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

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

Все методы поиска в 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(); 

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

Обновить людей

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

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

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

Удалить людей

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

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

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

Заключение

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

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

Полный пример проекта можно найти на GitHub .

comments powered by Disqus