express02.md 13 KB

Подключение к БД

Продолжаем разработку АПИ для проекта "ресторан" используя express.js.

Содержание:

Для подключения к БД можно использовать пакет mysql, но для более-менее сложных проектов, подразумевающих дальнейшее развитие, лучше использовать библиотеки, поддерживающие миграции (инициализация и изменение структуры базы данных). Для JavaScript наиболее популярна ORM библиотека sequelize. На хабре есть цикл статей, посвящённый этой библиотеке.

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

Установим зависимости

Выполняем в каталоге с проектом

npm i mysql2 sequelize sequelize-cli
  • Пакет mysql2 нужен для работы с БД MySQL (явно мы его не используем, но он нужен для sequelize)
  • Пакет sequelize нужен для использования в нашем коде
  • Консольная утилита sequelize-cli нужна для создания и управления миграциями

Инициализация и настройка sequelize (только перед первым запуском)

Инициализация:

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"
  }
}

То есть для разных сценариев мы можем использовать разные БД:

  • development - режим разработки
  • test - тестирование
  • production - "боевая" БД

Хранить логин/пароль к БД в открытом доступе нельзя, поэтому перепишите 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** можно задать переменные в настройках:

![](./launch_json.png)

![](./debugger.png)

И в разделе `"configurations"` добавьте объект `"env"`:

![](./env.png)

## Создание базы данных

>Базу можно создать и в менеджере баз данных, но сделаем в консоли, чтобы проверить все настройки

Для создания БД используется команда:

npx sequelize-cli db:create [--url=<строка подключения>]


Параметр `--url <строка подключения к БД>` можно не указывать, если:

* оставили стандартный вариант инициализации БД или в переменных окружения есть `DATABASE_URL` (учитывайте, что на командную строку не распространяются настройки VSCode)

* в корне проекта создан файл `.sequelizerc`, в котором задана переменная `url`:

    ```js
    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`:

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` для хранения блюд (тут вроде всё более менее понятно, подробно расписывать не буду - если что-то не понятно, то можно загуглить):

js '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


Дополнительные примеры создания миграций:

js 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