Понятие о миграциях в TypeORM

Node

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

Чтобы быстро освоиться с TypeORM, MySQL и ExpressJS, нам нужен проект. Если у вас нет готового TypeORM-проекта, давайте создадим его, следуя пошаговой инструкции:

Проект: пошаговая инструкция

1. Устанавливаем TypeORM

npm install typeorm

2. Инициализируем

typeorm init --name user-microservice --database mysql --express

Этой командой создаётся новый TypeORM-проект с названием user-microservice и выполняется автоматическая кодогенерация для использования Express и MySQL.

Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020Вот наш проект на Node.js с маршрутами, контроллером и сущностью

routes.ts — это точка входа для API. UserController.ts— оркестратор между маршрутами и сущностью. User.ts— сущность, определяющая схему таблицы для базы данных.

3. Устанавливаем пакеты

cd user-microservice
npm install

4. Конфигурации базы данных

Если у вас уже есть MySQL, запустите его локальный экземпляр и обновите параметры соединения в ormconfig.json.

А если нет, используйте следующую команду в Docker:

docker run --name songtham-mysql -e MYSQL_ROOT_PASSWORD=test -e MYSQL_USER=test -e MYSQL_PASSWORD=test -e MYSQL_DATABASE=test -p 3306:3306 -d mysql:latest --default-authentication-plugin=mysql_native_password

Этой Docker командой берётся последняя версия MySQL и запускается локально на компьютере. Внимание: при её запуске вам не потребуется вносить никаких изменений в ormconfig.json: всё уже есть в стандартном файле конфигурации.

5. Запускаем

npm start

А теперь проясним кое-что важное для понимания материала, изложенного далее:

  • Объектно-реляционное отображение (ORM) — это мост между API и базой данных
Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020
  • Сущность, называющаяся также моделью данных, представляет собой класс TypeORM, который хранит в базе данных состояние объекта. Её изменение обновит схему во время синхронизации или миграции.

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

Необходимые условия

  • Готовый TypeORM-проект, подключенный к базе данных (см. выше).

План статьи

  1. Synchronize
  2. Миграции
  3. Заключение

Synchronize

Прежде чем переходить к обсуждению миграций в TypeORM, сначала надо пару слов сказать о synchronize. Начнём с файла конфигурации ormconfig.json, который генерируется при инициализации нового TypeORM-проекта с typeorm init.

{
   "type": "mysql",
   "host": "localhost",
   "port": 3306,
   "username": "test",
   "password": "test",
   "database": "test",
   "synchronize": true,
   "logging": false,
   "entities": [
      "src/entity/**/*.ts"
   ],
   "migrations": [
      "src/migration/**/*.ts"
   ],
   "subscribers": [
      "src/subscriber/**/*.ts"
   ],
   "cli": {
      "entitiesDir": "src/entity",
      "migrationsDir": "src/migration",
      "subscribersDir": "src/subscriber"
   }
}

Заметим, что synchronize: true. Это значение по умолчанию, но что конкретно под ним подразумевается? И как оно связано с миграциями? В файле TypeORM README.md сказано:

«Synchronizeобеспечивает синхронизацию сущностей с базой данных при каждом запуске приложения».

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

Дальше рассмотрим три сценария синхронизации:

  1. Добавление новой колонки к сущности.
  2. Создание новой таблицы на основе новой сущности.
  3. Удаление колонки и/или таблицы через изменение сущности.

Посмотрим, что у нас получается в каждом из сценариев.

1. Добавление новой колонки к сущности

Перейдя на User.ts, увидим такой код:

import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";

@Entity()
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    age: number;

}

Добавим к сущности User новую колонку birthplace, копируя и вставляя вот это:

@Column()

birthplace: string;

Сохраняем. Перезагружаем локальный сервер и видим, как в таблице User появилась колонка birthplace.

Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020
Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020Сравните до и после: новая колонка birthplace в красном прямоугольнике.

Вот и всё. Теперь вы видите, как легко в базу данных добавляется новая колонка с помощью synchronize в TypeORM.

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

2. Создание новой таблицы на основе новой сущности

Для получения новой сущности создаём новый файл с названием Company.ts в одной папке с User.ts и вставляем вот в такой код:

import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";

@Entity()
export class Company {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

}

Сохраняем. Перезагружаем локальный сервер и видим, как в базе данных появилась таблица Company с колонками id и name.

Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020
Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020Создание новой таблицы на основе новой сущности

3. Удаление колонки и/или таблицы через изменение сущности

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

Итак, если synchronize автоматически синхронизирует код с базой данных, то так ли необходимы миграции?

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

Автоматическое обновление производственной схемы с синхронизацией таит в себе множество опасностей. Что, если данные будут потеряны или что-нибудь сломается? И как контролировать версии и изменения в схеме базы данных?

Вот здесь к нам и приходят на помощь миграции в TypeORM.

Миграции

Не пропускайте этот этап. Первым делом нужно установить synchronize: false в ormconfig.json. Это предотвратит синхронизацию схемы.

Дальше рассмотрим три сценария миграции:

  1. Генерирование миграции.
  2. Запуск миграции.
  3. Отмена изменений миграции

Начнём с первого сценария:

1. Генерирование миграции

Для создания файла миграции надо внести изменения в сущность. Открываем Company.ts и добавляем новую колонку:

@Column()

city: string;

Сохраняем. Пробуем перезапустить сервер. База данных не должна обновиться, потому что у нас synchronize установлен как false.

Дальше используем командную строку, набираем typeorm migration:generate. Этой командой генерируется новый файл миграции с SQL, выполнение которого необходимо для обновления схемы. После того, как мы его запускаем, появляется справочное меню, так как мы не указали аргумент name.

Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020Генерирование миграции в Typeorm

Аргумент name — это название класса миграции. Оно должно быть таким же информативным, как гит коммит. В нашем примере аргумент имеет такое название: AddCityColumnToCompany. Проблема в том, что при запуске следующей команды у нас бы вылезла ошибка:

Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020Неожиданная ошибка!

Это известная проблема, связанная с попытками среды node загрузить .ts вместо .js. Чтобы быстро устранить эту ошибку, запускаем такую команду:

./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:generate -n AddCityColumnToCompany

Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020Генерирование миграции в Typeorm

Если всё пройдёт хорошо, то в папке миграции вы увидите новый автоматически сгенерированный файл {TIMESTAMP}-AddCityColumnToCompany.ts с вот таким содержимым:

import {MigrationInterface, QueryRunner} from "typeorm";

export class AddCityColumnToCompany1576405409745 implements MigrationInterface {

    public async up(queryRunner: QueryRunner): Promise<any> {
        await queryRunner.query("ALTER TABLE `company` ADD `city` varchar(255) NOT NULL");
    }

    public async down(queryRunner: QueryRunner): Promise<any> {
        await queryRunner.query("ALTER TABLE `company` DROP COLUMN `city`");
    }

}

Обратите здесь внимание на два метода: (1) up (2) down. Это команды SQL.

  1. upсодержит код, необходимый для осуществления миграции.
  2. down содержит код для отмены изменений, сделанных командой up.

Отлично! Теперь у нас есть сгенерированный файл миграции. Запускаем его.

2. Запуск миграции

Запускаем миграцию следующей командой:

./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:run
Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020Запуск миграции в Typeorm

Если всё получилось, миграции запустят код в методе up. Проследим весь код строчку за строчкой и узнаем, какие инструкции в нём выполняются:

1. SELECT * FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ‘test’ AND `TABLE_NAME` = ‘migrations’: эта проверяет, есть ли в базе данных таблица миграций.

2. CREATE TABLE `test`.`migrations` (`id` int NOT NULL AUTO_INCREMENT, `timestamp` bigint NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`)) SONGTHAM ENGINE=InnoDB: если таблицы нет, создаём её.

3. SELECT * FROM `test`.`migrations` `migrations`: эта инструкция проверяет таблицу миграций на совпадение с названием файла миграции. Если совпадение находится, идём дальше. Если нет, продолжаем.

0 миграций уже загружены в базу данных.1 миграций найдены в исходном коде.
1 миграций новые миграции, которые должны быть выполнены.

4. query: START TRANSACTION
query: ALTER TABLE `company` ADD `city` varchar(255) NOT NULL: а эта инструкция SQL запускает скрипт миграции.

5. INSERT INTO `test`.`migrations`(`timestamp`, `name`) VALUES (?, ?) —  PARAMETERS: [1576405409745,”AddCityColumnToCompany1576405409745″]: при успешном запуске миграции эта инструкция вносит в таблицу миграции новую запись. Благодаря этому логу, если потребуется снова запустить команду миграции, TypeORM пропустит запуск этой миграции, так как третья инструкция показала, что миграция уже была запущена ранее.

Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020Нет ожидающих миграций

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

Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020Теперь в таблице company есть таблица миграций и колонка city.

3. Отмена изменений миграции

Наконец, переходим к последнему сценарию, рассматриваемому в этом руководстве.

Отмена изменений миграции запускает метод downв файле миграции. Для чего делать отмену изменений миграции? Например, мы внесли какое-то изменение в схему, но теперь хотим от него избавиться.

Используем такую команду:

./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:revert
Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020Отмена изменений миграции в Typeorm

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

1. query: SELECT * FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ‘test’ AND `TABLE_NAME` = ‘migrations’
query: SELECT * FROM `test`.`migrations` `migrations`: эта инструкция проверяет таблицу миграций на совпадение с названием файла миграции. Если совпадения есть, выполняем отмену.

1 миграций уже загружены в базу данных.
AddCityColumnToCompany1576405409745 — это последняя выполненная миграция.Была выполнена в воскресенье, 15 декабря 2019 года, в 17:23:29 по индокитайскому стандартному времени (+ 7 часов ко времени по Гринвичу).Выполняем отмену…

2. query: START TRANSACTION
query: ALTER TABLE `company` DROP COLUMN `city`: эта инструкция SQL запускает скрипт отмены миграции.

(3)query: DELETE FROM `test`.`migrations` WHERE `timestamp` = ? AND `name` = ? —  PARAMETERS: [1576405409745,”AddCityColumnToCompany1576405409745″]: с помощью этой инструкции запись удаляется из таблицы миграции. В этом случае при попытке отменить миграцию у нас бы ничего не произошло: TypeORM просто не нашёл бы записи миграции в базе данных. Однако при желании мы можем запустить миграцию заново.

Понятие о миграциях в TypeORM — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020Теперь таблица миграций пуста, а колонка city удалена из таблицы company

Заключение

Вот и всё. Поздравляем всех дочитавших до конца. Это было нелегко: вы только что сгенерировали, запустили и отменили миграции с помощью TypeORM.

Миграции — это важное средство управления обновлениями схемы базы данных. Здорово, что больше нет необходимости выполнять SQL инструкции вручную: теперь всё можно сделать в коде, осуществляя при этом контроль версий. Ну и появляется возможность объединить всё это с процессом непрерывной интеграции и развёртывания приложений, ведь при запуске TypeORM-миграций участвует командная строка.

Дополнительные ссылки

Специально для сайта ITWORLD.UZ. Новость взята с сайта NOP::Nuances of programming