Миграция в TypeORM — это единый файл с SQL-запросами для обновления схемы базы данных. Об этом важно знать администратору базы данных, бекэнд-инженеру или техлиду, так как это один из самых безопасных способов внесения изменений в базу данных в эксплуатационной среде.
Чтобы быстро освоиться с TypeORM, MySQL и ExpressJS, нам нужен проект. Если у вас нет готового TypeORM-проекта, давайте создадим его, следуя пошаговой инструкции:
Этой командой создаётся новый TypeORM-проект с названием user-microservice
и выполняется автоматическая кодогенерация для использования Express и MySQL.
routes.ts
— это точка входа для API. UserController.ts
— оркестратор между маршрутами и сущностью. User.ts
— сущность, определяющая схему таблицы для базы данных.
Если у вас уже есть 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
: всё уже есть в стандартном файле конфигурации.
А теперь проясним кое-что важное для понимания материала, изложенного далее:
Замечание: в статье я ссылаюсь на MySQL, вы же можете использовать другие базы данных.
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
обеспечивает синхронизацию сущностей с базой данных при каждом запуске приложения».
Другими словами, как только вы вносите изменения в сущность, автоматически происходит обновление изменений схемы с базой данных, привязанной к приложению.
Дальше рассмотрим три сценария синхронизации:
Посмотрим, что у нас получается в каждом из сценариев.
Перейдя на 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.
Используя аналогичный подход, можно даже создать новую таблицу.
Для получения новой сущности
создаём новый файл с названием Company.ts
в одной папке с User.ts
и вставляем вот в такой код:
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
@Entity()
export class Company {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
Сохраняем. Перезагружаем локальный сервер и видим, как в базе данных появилась таблица Company
с колонками id
и name
.
При удалении таблиц и колонок соблюдается та же последовательность действий: вносим в код изменения, сохраняем и перезагружаем сервер.
Итак, если synchronize
автоматически синхронизирует код с базой данных, то так ли необходимы миграции?
Миграции необходимы по той причине, что обычно использовать synchronize:true
для синхронизации схемы в эксплуатационной среде рискованно.
Автоматическое обновление производственной схемы с синхронизацией
таит в себе множество опасностей. Что, если данные будут потеряны или что-нибудь сломается? И как контролировать версии и изменения в схеме базы данных?
Вот здесь к нам и приходят на помощь миграции в TypeORM.
Не пропускайте этот этап. Первым делом нужно установить synchronize: false
в ormconfig.json
. Это предотвратит синхронизацию схемы.
Дальше рассмотрим три сценария миграции:
Начнём с первого сценария:
Для создания файла миграции надо внести изменения в сущность. Открываем Company.ts
и добавляем новую колонку:
@Column()
city: string;
Сохраняем. Пробуем перезапустить сервер. База данных не должна обновиться, потому что у нас synchronize
установлен как false
.
Дальше используем командную строку, набираем typeorm migration:generate
. Этой командой генерируется новый файл миграции с SQL, выполнение которого необходимо для обновления схемы. После того, как мы его запускаем, появляется справочное меню, так как мы не указали аргумент name
.
Аргумент name
— это название класса миграции. Оно должно быть таким же информативным, как гит коммит. В нашем примере аргумент имеет такое название: AddCityColumnToCompany
. Проблема в том, что при запуске следующей команды у нас бы вылезла ошибка:
Это известная проблема, связанная с попытками среды node
загрузить .ts
вместо .js
. Чтобы быстро устранить эту ошибку, запускаем такую команду:
./node_modules/.bin/ts-node ./node_modules/.bin/typeorm migration:generate -n AddCityColumnToCompany
Если всё пройдёт хорошо, то в папке миграции вы увидите новый автоматически сгенерированный файл {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.
up
содержит код, необходимый для осуществления миграции.down
содержит код для отмены изменений, сделанных командой up
.Отлично! Теперь у нас есть сгенерированный файл миграции. Запускаем его.
Запускаем миграцию следующей командой:
./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
.
Наконец, переходим к последнему сценарию, рассматриваемому в этом руководстве.
Отмена изменений миграции запускает метод 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 TRANSACTIONquery: 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-миграций участвует командная строка.
Перевод статьи
Комментарии