Продолжаем разработку АПИ для проекта "ресторан" используя express.js.
Содержание:
Для подключения к БД можно использовать пакет mysql, но для более-менее сложных проектов, подразумевающих дальнейшее развитие, лучше использовать библиотеки, поддерживающие миграции (инициализация и изменение структуры базы данных). Для JavaScript наиболее популярна ORM библиотека sequelize. На хабре есть цикл статей, посвящённый этой библиотеке.
Всеми возможностями мы пользоваться не будем, нам достаточно миграций.
Выполняем в каталоге с проектом
npm i mysql2 sequelize sequelize-cli
mysql2 нужен для работы с БД MySQL (явно мы его не используем, но он нужен для sequelize)sequelize нужен для использования в нашем кодеsequelize-cli нужна для создания и управления миграциямиnpx sequelize-cli init
Будут созданы следующие каталоги:
config — в этом каталоге лежит файл с настройками подключения к БДmodels — модели для проектаmigrations — файлы с миграциямиseeders — файлы для заполнения БД начальными даннымиДалее нам нужно сообщить консольной утилите (sequelize-cli), как подключаться к БД. Для этого откроем файл config/config.json. Он выглядит примерно так:
{
"development": {
"username": "root",
"password": null,
"database": "database_development",
"host": "127.0.0.1",
"dialect": "mysql"
},
"test": {
"username": "root",
"password": null,
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": null,
"database": "database_production",
"host": "127.0.0.1",
"dialect": "mysql"
}
}
То есть для разных сценариев мы можем использовать разные БД:
Хранить логин/пароль к БД в открытом доступе нельзя, поэтому перепишите config/config.json таким образом:
{
"development": {
"use_env_variable": "DATABASE_URL",
"dialect": "mysql"
},
"test": {
"use_env_variable": "DATABASE_URL",
"dialect": "mysql"
},
"production": {
"use_env_variable": "DATABASE_URL",
"dialect": "mysql"
}
}
В каталоге models есть файл index.js в котором происходит инициализация подключения, нам в нём ничего менять не нужно, но я приведу кусок кода из него, чтобы было понятно где используется параметр use_env_variable
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
То есть, если в конфиге есть параметр use_env_variable, то в конструктор передается строка подключения из переменной окружения, которая задана в этом параметре (в нашем случае DATABASE_URL). Иначе используется конструктор с параметрами database, username и password.
Итак, Sequelize поддерживает загрузку строки подключения из переменных окружения. Нам нужно создать переменную окружения в таком формате:
DATABASE_URL=mysql://user:password@sqldomain/db_name
В VSCode можно задать переменные в настройках:
И в разделе "configurations" добавьте объект "env":
Базу можно создать и в менеджере баз данных, но сделаем в консоли, чтобы проверить все настройки
Для создания БД используется команда:
npx sequelize-cli db:create [--url=<строка подключения>]
Параметр --url <строка подключения к БД> можно не указывать, если:
оставили стандартный вариант инициализации БД или в переменных окружения есть DATABASE_URL (учитывайте, что на командную строку не распространяются настройки VSCode)
в корне проекта создан файл .sequelizerc, в котором задана переменная url:
module.exports = {
'url': 'mysql://ekolesnikov:<тут пароль>@kolei.ru/restaurant'
}
Такой вариант предпочтительнее, чтобы не писать url при каждой миграции (мы рассматриваем вариант, когда в конфиге не хранятся логин и пароли, а используется переменная окружения). Только не забудьте и этот файл прописать в .gitignore
Если всё настроили правильно, то при запуске команды npx sequelize-cli db:create выдаст примерно такое (и на сервере MySQL будет создана БД, которая указана в строке подключения):
Sequelize CLI [Node: 23.9.0, CLI: 6.6.3, ORM: 6.37.7]
Parsed url mysql://ekolesnikov:*****@kolei.ru/restaurant
Database restaurant created.
Миграции создаются командой:
npx sequelize-cli migration:generate --name first
Вместо
firstлучше писать что-то человеко-понятное, напримерcreate_MenuItem
В каталоге migrations будет создан файл YYYYMMDDhhmmss-first.js:
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
/**
* Add altering commands here.
*
* Example:
* await queryInterface.createTable('users', { id: Sequelize.INTEGER });
*/
},
async down (queryInterface, Sequelize) {
/**
* Add reverting commands here.
*
* Example:
* await queryInterface.dropTable('users');
*/
}
};
В методе up мы должны прописать команды для создания таблиц и связей, а в методе down для удаления. В первой миграции мы создадим таблицу MenuItem для хранения блюд (тут вроде всё более менее понятно, подробно расписывать не буду - если что-то не понятно, то можно загуглить):
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.createTable('MenuItem', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.DataTypes.INTEGER
},
title: {
type: Sequelize.DataTypes.STRING,
allowNull: false,
comment: 'название блюда'
},
image: {
type: Sequelize.DataTypes.STRING,
allowNull: false,
comment: 'название файла с изображением блюда'
},
description: {
type: Sequelize.DataTypes.TEXT,
allowNull: true,
comment: 'описание блюда'
},
price: {
type: Sequelize.DataTypes.INTEGER,
allowNull: false
}
})
},
async down (queryInterface, Sequelize) {
await queryInterface.dropTable('MenuItem')
}
}
И "накатим" её командой:
npx sequelize-cli db:migrate [--url <строка подключения к БД>]
Логи консоли:
> npx sequelize-cli db:migrate
Sequelize CLI [Node: 23.9.0, CLI: 6.6.3, ORM: 6.37.7]
Parsed url mysql://ekolesnikov:*****@kolei.ru/restaurant
== 20250514112031-first: migrating =======
== 20250514112031-first: migrated (0.240s)
Если вдруг что-то забыли, то можно "откатить" последнюю миграцию командой:
npx sequelize-cli db:migrate:undo
Дополнительные примеры создания миграций:
async up (queryInterface, Sequelize) {
// создаём словарь
await queryInterface.createTable('NewsType', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.DataTypes.INTEGER
},
title: {
type: Sequelize.DataTypes.STRING(16),
allowNull: false
},
})
// заполняем словарь данными
await queryInterface.bulkInsert('NewsType',[
{id: 1, title: 'Срочное'},
{id: 2, title: 'Важное'}
])
// создаём таблицу, в которой будет использоваться словарь
await queryInterface.createTable('News', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.DataTypes.INTEGER
},
// поле для внешнего ключа
newsTypeId: {
type: Sequelize.DataTypes.INTEGER,
allowNull: false
}
// тут еще много всяких полей
})
// создаём внешний ключ
await queryInterface.addConstraint('News', {
fields: ['newsTypeId'],
type: 'foreign key',
// названия ключей должны быть уникальными
name: 'FK_News_NewsType',
references: {
table: 'NewsType',
field: 'id'
}
})
// Пример создания таблицы связей (многие-ко-многим)
// особенность в том, что первичный ключ в таких таблицах составной
// и создаётся отдельно
await queryInterface.createTable('NewsRole', {
newsId: {
type: Sequelize.DataTypes.INTEGER,
allowNull: false
},
roleId: {
type: Sequelize.DataTypes.INTEGER,
allowNull: false
}
})
// добавление первичного ключа
await queryInterface.addConstraint('NewsRole', {
fields: ['newsId', 'roleId'],
type: 'primary key',
name: 'PK_NewsRole'
})
// далее создаются внешние ключи для полей newsId и roleId
}
sequelizeсоздайте миграцию с командами создания таблиц и связей между ними (заполните словари, если они есть)
Этот пункт делать только после утверждения ERD