MongoDB
Основи MongoDB
MongoDB є нереляційною базою даних типу NoSQL. База заснована на моделі документів – об'єкти даних зберігаються у вигляді окремих документів у колекції
Колекція та документ
Дані у MongoDB групуються в колекції. Колекція – це збірка документів, які мають однакове призначення. Колекція подібна до таблиці в SQL базі даних, але відрізняється тим, що для колекції немає суворої схеми та документи колекції можуть мати різну структуру.
Максимальний розмір документа обмежений 16 Мб.
У MongoDB у якості мови запитів використовується JavaScript та JSON-структури. Вибір мови запиту пояснюється тим, що MongoDB використовує JSON-формат для представлення документів та виведення результатів. Фізично JSON-структури зберігаються у бінарному BSON-форматі.
Документи (тобто об'єкти) відповідають власним типам даних у багатьох мовах програмування. Вбудовані документи та масиви скорочують потребу в дорогих об'єднаннях.
У MongoDB первинний ключ автоматично встановлюється у полі _id
Фактично змінна _id – це об'єкт типу ObjectId
_id: ObjectId('5f15996fbbde793a107af359');
Він містить 12 байт, кожен з яких формується певним чином.
- 4 - байтове значення (5f15996f), що позначає секунди, починаючи з останнього запису
- 3 - байтове значення (bbde79), що позначає ідентифікатор машини
- 2 - байтове значення (3a10), що позначає ідентифікатор процесу
- 3 - байтовий лічильник (7af359), починаючи з випадкового значення
MongoDB Atlas
Замість встановлення бази даних локально, ми використовуватимемо хмарне сховище MongoDB Atlas .
Пройдіть реєстрацію, найкраще прив'язавши до хмари свій обліковий запис Google. Створіть свій перший кластер. Вибирайте регіон із безкоштовним кластером M0 Sandbox, розміром 512 Мб. Якщо все пройшло успішно, у вас повинен бути доступ до dashboard

Спершу перейдіть в лівому меню на вкладку Network Access і додайте IP адреси, у яких буде доступ до хмарної бази даних.

Як варіант можна дозволити доступ із будь-якої IP адреси 0.0.0.0/0. Але можете вказати лише необхідні вам адреси.
Другим кроком необхідно створити користувача для нашої бази даних. Цей обліковий запис ми будемо використовувати для доступу до бази даних з нашого застосунку та графічного інтерфейсу. Переходимо у вкладку Database Access і вибираємо Add New Database User. Вибираємо метод аутентифікації за допомогою пароля. Придумайте ім'я та пароль користувачеві і запам'ятайте, ми помістимо їх надалі в змінні оточення. Також виставте привілеї доступу як читання та запис до бази даних.
Попереднє налаштування завершено і тепер можна повернутися на вкладку Cluster. Тут вибираємо Connect. У вспливаючому вікні вибираємо Connect your application

Потрібно вибрати драйвер для Node.js. Нам нададуть рядок для доступу до бази даних
mongodb+srv://;username;:;password;@krabaton.5mlpr.gcp.mongodb.net/;dbname;?retryWrites=true&w=majority
За допомогою цього рядка ми з'єднуватимемося з нашою хмарною базою даних
MongoDB GUI
MongoDB Compass
Підключення до бази даних здійснюється за допомогою рядка SRV

Необхідно вставити рядок із вже підставленими плейсхолдерами у блок підключення Compass та натиснути кнопку connect

Основні команди MongoDB
Це для Robo 3T

Mongoose
Mongoose представляє спеціальну ODM-бібліотеку (Object Data Modelling) для роботи з MongoDB. Найчастіше бібліотека Mongoose слугує зручним засобом для застосування структурованої схеми до колекції MongoDB. Модуль слугує зручним засобом застосування структурних схем до документів. Також дає можливість достовірної перевірки типів даних та можливостей валідації.
Офіційну документацію можна подивитися тут
mongoose.Promise = global.Promise;
const mongoose = require('mongoose');
Підключення до бази виконується методом mongoose.connect(), в який першим параметром передається адреса на підключення до бази даних, а другим – об'єкт налаштувань:
mongoose.connect(process.env.DB_HOST, { useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true, });
За допомогою методу mongoose.disconnect() можна відключитися від бази даних.
Схема документа
Однією з переваг ODM Mongoose є те, що дані можна описати певною схемою. Експортуємо клас Schema
const Schema = mongoose.Schema;
Встановлюємо схему
const cats = new Schema({ nickname: String, age: Number, });
Для типів даних можна вказувати одне з наступних значень:
- String
- Number
- Date
- Buffer
- Boolean
- Mixed
- Objectid
- Array
Для складних властивостей як об'єкт, замість типу вказується визначення цього об'єкта: Приклад:
const cats = new Schema({ nickname: String, age: Number, owner: { name: String, address: [String], // тип - масив рядків birthday: Date, }, });
При визначенні схеми Mongoose має вбудовані правила валідації, які можна вказати у схемі:
- required: вимагає обов'язкової наявності значення для властивості
- min та max: задають мінімальне та максимальне значення для числових даних
- minlength та maxlength: задають мінімальну та максимальну довжину для рядків
- enum: рядок повинен представляти одне зі значень у зазначеному масиві рядків
- match: рядок повинен відповідати регулярному виразу
const cats = new Schema({ nickname: { type: String, minlength: 2, maxlength: 7, required: [true, 'Nickname is required'], }, age: { type: Number, min: 1, max: 50, }, owner: { name: String, address: [String], // тип - масив рядків birthday: Date, }, });
Якщо ми спробуємо додати некоректні дані в БД, то запит на додавання поверне помилку.
Після цього необхідно створити модель, використовуючи схему:
const Cat = mongoose.model('cat', cats);
Перший параметр у методі mongoose.model вказує на назву моделі, а другий параметр – власне схема.
const cat = new Cat({ nickname: 'Barsik', age: 1, });
Для збереження об'єкта в базі викликається метод save. Він визначений для всіх створюваних моделей та зберігає поточний об'єкт у базу даних. Метод повертає результат – о 'єкт типу Document, який представляє конкретний документ, що зберігається в колекції.
const result = cat.save(); console.log('Кіт збережений у базу! ', result);
Індекси
Якщо поле часто використовується при пошуку документів йому можна призначити індекс. Індексування полів дозволяє швидше шукати за цими полями. Індекс до поля можна додати двома способами.
Перший – визначити у самій схемі
const cats = new Schema({ nickname: { type: String, index: 1 }, age: Number, });
Або викликавши метод index у самій схемі
const cats = new Schema({ nickname: String, age: Number, }); cats.index({ nickname: 1 });
Унікальні поля
Значення поля можна зробити унікальним. Це означає, що в інших документах колекції не може бути такого поля з таким самим значенням. Наприклад, логічно зробити поле email, для схеми, яка описує користувача, унікальним. Для цього потрібно у схемі, при визначенні поля, додати властивість unique
const user = new Schema({ username: String, email: { type: String, unique: true }, });
Обов'язкові поля
const user = new Schema({ username: { type: String, required: true }, email: { type: String, unique: true, required: true }, });
Методи в об'єкта схеми
const user = new Schema({ firstName: String, lastName: String, }); user.methods.fullName = function () { return ${this.firstName} ${this.lastName}; };
Отримавши потім екземпляр документа з бази, ми можемо викликати в нього функцію fullName(), яка повертає повне ім'я та прізвище користувача
Основні операції з даними в Mongoose
Створення документів
Крім розглянутого методу save(), також можна використати метод від моделі об'єкта Cat.create(). Першим параметром методу передається об'єкт для збереження.
Cat.create({ nickname: 'Barsik', age: 1, });
Отримання даних
Для отримання даних можна використати методи
-
find([query], [options]);
Встановлює операцію пошуку, яка повертає масив об'єктів типу Document, які збігаються із запитом -
findOne([query], [options]);
Встановлює операцію пошуку одного документа, що повертає перший об'єкт Document, який збігається із запитом
Видалення даних
Для видалення застосовуються наступні методи
-
remove([query], [options]);
Встановлює операцію видалення, під час якої з колекції видаляються всі об'єкти, що збігаються із запитом -
findOneAndRemove([query], [options]);
Встановлює операцію пошуку та видалення, під час якої з колекції видаляється перший же документ, що збігається із запитом
Зміна даних
-
update([query], [update], [options]);
Встановлює операцію оновлення, в ході якої в колекції оновлюються всі документи, що збігаються із запитом -
findOneAndUpdate([query], [update], [options]);
Встановлює операцію пошуку та оновлення, під час якої в колекції оновлюється перший же документ, що збігається із запитом.
Підключення Mongoose
Давайте тепер розберемо REST API застосунок та підключення до нього Mongoose. Структура нашого застосунку буде наступною
├── server.js ├── .env ├── controller │ └── index.js ├── package.json ├── service │ ├── schemas │ | └── task.js │ └── index.js └── api └── index.js
З самим застосунком ви можете ознайомитись: glitch.com
API доступно за цим URL: https://nodebook-api-mongoose.glitch.me/api/tasks
- Роботу з базою даних ми виділяємо в окремий сервіс та поміщаємо його папку service, файл index.js.
- Визначаємо схеми для документів у папці schemas.
- Роути у нас залишаться як і раніше у папці api
- логіку роботи застосунку ми перенесемо у папку controller
Давайте тепер поговоримо детальніше про кожний модуль застосунку
Головний файл та підключення до БД
Файл сервера server.js. Підключаємо необхідні модулі та створюємо екземпляр застосунку.
const express = require('express'); const cors = require('cors'); const mongoose = require('mongoose'); require('dotenv').config(); const app = express();
Підключаємо парсер JSON і дозволяємо кросдоменні запити до нашого застосунку через проміжне ПЗ cors
// parse application/json app.use(express.json()); // cors app.use(cors());
Підключаємо роути для нашого API, а також обробку помилки 404 та помилок сервера 500
const routerApi = require('./api'); app.use('/api', routerApi); app.use((_, res, __) => { res.status(404).json({ status: 'error', code: 404, message: 'Use api on routes: /api/tasks', data: 'Not found', }); }); app.use((err, _, res, __) => { console.log(err.stack); res.status(500).json({ status: 'fail', code: 500, message: err.message, data: 'Internal Server Error', }); });
Підключаємось до сервера MongoDB за допомогою mongoose.connect. Цей метод повертає проміс, і коли він вирішиться, ми стартуємо наш сервер app.listen. Якщо ж під час підключення до бази даних сталася помилка, то стартувати сервер немає жодної причини і ми виводимо в консоль просто повідомлення про помилку.
const PORT = process.env.PORT || 3000; const uriDb = process.env.DB_HOST; const connection = mongoose.connect(uriDb, { promiseLibrary: global.Promise, useCreateIndex: true, useUnifiedTopology: true, useFindAndModify: false, }); connection .then(() => { app.listen(PORT, function () { console.log(Server running. Use our API on port: ${PORT}); }); }) .catch(err => console.log(Server not running. Error message: ${err.message}), );
Роуты
Файл роутингу api/index.js у нас став лаконічним та чистим
const express = require('express'); const router = express.Router(); const ctrlTask = require('../controller'); router.get('/tasks', ctrlTask.get); router.get('/tasks/:id', ctrlTask.getById); router.post('/tasks', ctrlTask.create); router.put('/tasks/:id', ctrlTask.update); router.patch('/tasks/:id/status', ctrlTask.updateStatus); router.delete('/tasks/:id', ctrlTask.remove); module.exports = router;
Ми імпортуємо контролер і для кожного маршруту та необхідного методу HTTP викликаємо відповідний метод контролера ctrlTask. При подальшій роботі над застосунком тут можуть з'явитися проміжні ПЗ для авторизації необхідних маршрутів, для валідації даних, що передаються, та інші допоміжні функції.
Контролери
Тут ми помістимо основну логіку роботи нашого застосунку. Фактично контролер у нашому випадку – якийсь диспетчер, який приймає запит на обробку маршруту від роуту, звертається до бази даних через сервіс та з отриманим результатом формує відповідь застосунку.
Щоб отримати список усіх завдань достатній простий обробник
const get = async (req, res, next) => { try { const results = await service.getAlltasks(); res.json({ status: 'success', code: 200, data: { tasks: results, }, }); } catch (e) { console.error(e); next(e); } };
Ми звертаємось до сервісу та запитуємо у бази всі поточні завдання нашого TODO списку
const results = await service.getAlltasks();
Потім відправляємо отриманий результат клієнту
res.json({ status: 'success', code: 200, data: { tasks: results, }, });
У разі помилки виконання ми надсилаємо її далі next(err), для обробника помилок у файлі server.js
Контролер для обробки запиту завдання за id схожий на попередній, але має важливу відмінність. У випадку, якщо сервіс нам нічого не повернув з бази даних, ми повертаємо відповідь 404 – нічого не знайдено
const getById = async (req, res, next) => { const { id } = req.params; try { const result = await service.getTaskById(id); if (result) { res.json({ status: 'success', code: 200, data: { task: result }, }); } else { res.status(404).json({ status: 'error', code: 404, message: Not found task id: ${id}, data: 'Not Found', }); } } catch (e) { console.error(e); next(e); } };
Так само ми робимо для контролерів оновлення завдання та видалення – якщо нічого не знайдено повертаємо помилку 404
Сервіс роботи з БД
Сервіс для виконання операцій над базою даних теж досить лаконічний
const Task = require('./schemas/task'); const getAlltasks = async () => { return Task.find(); }; const getTaskById = id => { return Task.findOne({ _id: id }); }; const createTask = ({ title, text }) => { return Task.create({ title, text }); }; const updateTask = (id, fields) => { return Task.findByIdAndUpdate({ _id: id }, fields, { new: true }); }; const removeTask = id => { return Task.findByIdAndRemove({ _id: id }); }; module.exports = { getAlltasks, getTaskById, createTask, updateTask, removeTask, };
У нас є п'ять функцій, які виконують всі основні операції для нашого простого API
Щоб отримати всі завдання, ми використовуємо метод find, який викликаємо у моделі та повертаємо результат у контролер
const getAlltasks = async () => { return Task.find(); };
Отримати конкретне завдання за id. Ми викликаємо метод findOne, який знаходить нам єдиний результат за умовою { _id: id }. Якщо метод нічого не знайде, то буде повернено значення null
const getTaskById = id => { return Task.findOne({ _id: id }); };
Створення нового завдання. Викликаємо у моделі метод create
const createTask = ({ title, text }) => { return Task.create({ title, text }); };
Оновлюємо завдання методом findByIdAndUpdate, першим параметром передаємо умову пошуку – збіг за id, а другим – об'єкт з полями, які необхідно оновити. Третій параметр вказує, що метод повинен повернути вже оновлений документ
const updateTask = (id, fields) => { return Task.findByIdAndUpdate({ _id: id }, fields, { new: true }); };
І остання операція видаляє завдання з бази даних. Використовуємо метод Mongoose findByIdAndRemove, якому передаємо id завдання, а метод знаходить та видаляє його з бази даних.
const removeTask = id => { return Task.findByIdAndRemove({ _id: id }); };
Схема
Останнім, що залишилося нам розглянути, – це файл створення схеми для нашої колекції завдань.
const {Schema, model} = require('mongoose'); const task = new Schema( { title: { type: String, minlength: 2, maxlength: 70, }, text: { type: String, minlength: 3, maxlength: 170, }, isDone: { type: Boolean, default: false, }, }, { versionKey: false, timestamps: true }, ); const Task = model('task', task); module.exports = Task;
В принципі, тут нам уже все зрозуміло, ми створюємо схему з трьома
полями title, text, isDone. Визначаємо тип значень, що зберігаються,
і накладаємо обмеження. З нового тут з'явився параметр із опціями при створенні схеми.
{ versionKey: false, timestamps: true }
Ці опції відключають версіонування документів встановленням значення якості versionKey у false. Mongoose за замовчуванням додає версіонування – параметр __v, який вказує на версію зміненого документа. Здебільшого це потрібно для документів зі складною структурою, а, оскільки структура нашої схеми плоска, версіонування ми відключаємо. Друга опція включає в нашу схему дві додаткові властивості: час створення документа createdAt та час оновлення updatedAt. Причому Mongoose автоматично буде встановлювати ці поля при створенні та змінювати поле updatedAt при кожному оновленні документа, що, погодьтеся, дуже зручно.
У результаті типовий документ у нашій колекції tasks повинен виглядати так:
{ "isDone": false, "_id": "5f8e3067975b9d23a0dbd270", "title": "My work", "text": "Pain and pain!", "createdAt": "2020-10-20T00:33:43.492Z", "updatedAt": "2020-10-20T00:43:16.961Z" }
Ми розібрали простий застосунок REST API та підключення бібліотеки Mongoose у нашому проекті