Миграции в Yii2: рабочие приёмы и типичные ошибки
Как писать миграции в Yii2 так, чтобы их можно было откатить, а на проде не поймать блокировку таблицы на миллион строк. Разбор safeUp, транзакций и порядка применения.

Вы добавили колонку в таблицу заказов через addColumn, накатили на прод — и MySQL заблокировал таблицу на время перестройки. Пять минут магазин не принимал заказы. Или проще: написали миграцию, а откатить её down() не смог, потому что вы забыли описать обратные действия. Миграции в Yii2 выглядят простыми, пока вы работаете в одиночку на пустой базе. На живом проекте с историей и данными всплывают детали, о которых консоль yii migrate не предупреждает.
Речь о Yii2 (ветка 2.0.x, актуальна на момент написания). Предполагаю, что консольное приложение у вас уже настроено, подключение к БД работает и ./yii migrate запускается без ошибок. Разбираем не «как создать миграцию» — это одна команда, — а как писать их так, чтобы они переживали откат и не роняли прод.
Откуда берутся проблемы
Yii2 хранит применённые миграции в таблице migration: имя класса и время применения. Команда ./yii migrate сравнивает файлы в каталоге миграций с этой таблицей и накатывает недостающие в порядке имён — а имена начинаются с временной метки m231015_120000_, поэтому порядок хронологический. Откат ./yii migrate/down идёт в обратном порядке и вызывает метод down() или safeDown() каждого класса.
Первая ловушка: если down() написан неверно или возвращает false, откат обрывается, а запись в таблице migration уже может быть удалена частично. Вторая: миграция, которая меняет большую таблицу напрямую, на MySQL до 5.6 или на некоторых операциях и в новых версиях приводит к копированию таблицы под блокировкой.
up/down против safeUp/safeDown
Базовый класс yii\db\Migration даёт две пары методов. Пара up()/down() выполняется как есть. Пара safeUp()/safeDown() оборачивает всё содержимое в транзакцию БД: если внутри что-то упало, изменения откатываются целиком.
Транзакция спасает от полусделанной миграции только там, где СУБД умеет транзакционный DDL. PostgreSQL умеет — тамsafeUpс несколькимиcreateTableлибо пройдёт весь, либо откатится весь. MySQL с InnoDB на DDL-командах (CREATE, ALTER, DROP) делает неявный commit, и транзакция там вас не защитит.
Практический вывод: на MySQL safeUp не даёт атомарности для схемных изменений, но даёт для изменений данных (INSERT, UPDATE). На PostgreSQL — даёт и для того, и для другого. Не полагайтесь на транзакцию, если целитесь в MySQL и меняете структуру.
Четыре приёма, которые окупаются
1. Всегда описывайте обратное действие
Если создали таблицу в safeUp — удалите её в safeDown. Если добавили колонку — уберите. Миграция без рабочего отката однажды заблокирует деплой: вы захотите откатиться на шаг назад, а система не сможет.
<?php
use yii\db\Migration;
// Yii2 2.0.x
class m231015_120000_add_status_to_order extends Migration
{
public function safeUp()
{
$this->addColumn('{{%order}}', 'status', $this->string(20)->notNull()->defaultValue('new'));
// индекс по новому полю, если по нему будут фильтровать
$this->createIndex('idx-order-status', '{{%order}}', 'status');
}
public function safeDown()
{
// обратный порядок: сначала индекс, потом колонку
$this->dropIndex('idx-order-status', '{{%order}}');
$this->dropColumn('{{%order}}', 'status');
}
}Порядок в safeDown обратен safeUp: сначала снимаем то, что зависит (индекс), потом то, от чего зависит (колонку). Синтаксис {{%order}} — это плейсхолдер префикса таблиц, Yii подставит настроенный tablePrefix.
2. Не хардкодьте данные, которые нельзя откатить
Если миграция вставляет справочные данные, в safeDown удаляйте ровно то, что вставили, по ключу — а не DELETE FROM всей таблицы. Иначе откат снесёт и то, что добавили пользователи.
3. Разделяйте схему и данные на разные миграции
Одна миграция создаёт таблицу или колонку, следующая наполняет данными. Причина — на MySQL DDL делает неявный commit, и если смешать создание колонки с обновлением миллиона строк в одном safeUp, то при падении обновления вы останетесь с колонкой без данных и без возможности откатить транзакцию.
4. Проверяйте существование перед изменением
На проде состояние базы бывает не таким, каким его ждёт миграция: кто-то накатил патч руками, колонка уже есть. Перед добавлением можно проверить схему через $this->db->getTableSchema() и его свойство с колонками. Это защищает от падения на повторном запуске.
Сравнение способов менять большую таблицу
Добавление колонки или индекса на таблице в миллионы строк — самая частая причина простоя из-за миграции. Варианты:
| Подход | Что выигрываете | Чем платите |
|---|---|---|
Прямой addColumn в миграции | Просто, одна команда | Возможная блокировка таблицы на время операции на MySQL |
| Колонка с nullable без default | На MySQL 8 и MariaDB часто мгновенно (INSTANT/inplace) | Не всегда доступно, зависит от типа изменения и версии |
| Онлайн-инструмент (pt-online-schema-change, gh-ost) вне Yii | Без блокировки на огромных таблицах | Отдельный инструмент, миграция уходит из-под контроля Yii |
Для действительно больших таблиц на MySQL миграция средствами Yii — не всегда правильный инструмент. Разумно применять схемное изменение отдельным online-инструментом, а в Yii оставить пустую миграцию-маркер, чтобы таблица migration не рассинхронизировалась с реальным состоянием. Точное поведение конкретной ALTER-операции (INSTANT, INPLACE, COPY) сверяйте в документации вашей версии MySQL или MariaDB — оно менялось между версиями.
Когда что выбирать
- Маленькая таблица, разработка или staging — прямой
addColumn/createTableвsafeUp, не усложняйте. - PostgreSQL любого размера —
safeUpс транзакцией, схема и данные можно смелее держать рядом, откат атомарен. - MySQL, таблица с сотнями тысяч строк и больше — схему отдельной миграцией, наполнение данными отдельной, следите за типом ALTER.
- MySQL, таблица в миллионы строк, прод без окна простоя — online-инструмент вне Yii, миграция-маркер для учёта.
Что ломается на практике
- Откат обрывается на середине. Если
safeDownпадает на второй операции, а первая уже сделала неявный commit (MySQL DDL), база остаётся в промежуточном состоянии. Пишите откат так, чтобы каждый шаг был идемпотентным или проверял состояние. - Две ветки, две миграции с близкими метками. После merge порядок применения определяется именами. Если в двух ветках создали миграции с зависимостью друг от друга, но метки перепутаны, накатится не тот порядок. Проверяйте метки при слиянии.
- Индекс с именем, которое уже занято. Yii не генерирует имя автоматически при
createIndex— вы задаёте его сами. Дубликат имени индекса в пределах таблицы уронит миграцию. - Foreign key при откате. Если удаляете таблицу, на которую есть внешний ключ из другой, сначала снимите ключ, потом удаляйте таблицу — иначе СУБД не даст.
Как проверить откат до прода
Единственный надёжный способ убедиться, что safeDown рабочий, — прогнать его на копии базы: накатить миграцию (./yii migrate/up 1), затем откатить (./yii migrate/down 1) и посмотреть, что схема вернулась в исходное состояние. Делать это на dev-базе, а не на проде.
Что делать дальше
Добавьте прогон миграций и их отката в CI на копию схемы — так вы поймаете нерабочий safeDown до деплоя, а не во время инцидента. На проде перед крупной схемной миграцией смотрите на размер таблицы и тип ALTER-операции в документации вашей СУБД. Инвалидация кеша схемы Yii после миграций — отдельная тема, здесь её не разбираем. Дальше стоит освоить ./yii migrate/mark для случаев, когда схему меняли в обход Yii и нужно синхронизировать таблицу migration.
Антиблок-аналитик 115-ФЗ: Анализ выписок, поиск рисков блокировки счёта, инструкции по безопасным транзакциям.
Спросить за 41 ₽Источники: Yii2 Guide: Database Migration, yii\db\Migration — API reference
Частые вопросы
У меня Yii1, а не Yii2 — приёмы подойдут?
Почему транзакция в safeUp не откатила изменения на MySQL?
Что будет, если я не напишу safeDown?
Миграция упала на середине, что делать?
Как добавить колонку в огромную таблицу без простоя?
Как безопасно отредактировать уже применённую миграцию?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.