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.

Вот наш проект на 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, который хранит в базе данных состояние объекта. Её изменение обновит схему во время синхронизации или миграции.

Замечание: в статье я ссылаюсь на 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.

Сравните до и после: новая колонка 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.

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

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

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

Неожиданная ошибка!

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

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

Генерирование миграции в 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

Если всё получилось, миграции запустят код в методе 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 пропустит запуск этой миграции, так как третья инструкция показала, что миграция уже была запущена ранее.

Нет ожидающих миграций

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

Теперь в таблице company есть таблица миграций и колонка city.

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

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

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

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

./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:revert
Отмена изменений миграции в 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 просто не нашёл бы записи миграции в базе данных. Однако при желании мы можем запустить миграцию заново.

Теперь таблица миграций пуста, а колонка city удалена из таблицы company

Заключение

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

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

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

Читайте также:


Перевод статей: Songtham Tung: TypeORM Migrations Explained и How to Create RESTful APIs on Node.js with TypeORM CLI

Предыдущая статьяКак сделать кастомные шорткаты для Siri
Следующая статьяХватит везде использовать ===