Миграции в Yii: где ломаются и как не потерять данные
Откат до нужной точки не срабатывает, foreign key не создаётся, а на проде команда падает с ошибкой прав. Разбираем типичные грабли миграций Yii 2 и что делать, чтобы схема на всех окружениях совпадала.

Схема базы разъехалась между разработчиком и продом, потому что кто-то поправил таблицу руками, а миграцию не написал. Или откат migrate/down не восстановил удалённый столбец, потому что в down() стоял пустой return false. Это два самых частых способа потерять час на разбор и, в худшем случае, данные. Разбираем грабли миграций в Yii 2.0.x, на которые наступают чаще всего, и как их обойти.
Как миграции устроены в Yii 2
Механизм простой: каждая миграция — класс, унаследованный от yii\db\Migration, с методами up() и down() (или safeUp() / safeDown(), обёрнутыми в транзакцию). Применённые миграции Yii записывает в таблицу migration — по умолчанию в основном соединении db. Команда yii migrate сравнивает файлы в каталоге с этой таблицей и накатывает недостающие.
Отсюда растут почти все проблемы: рассинхрон между таблицей migration и реальной схемой, откаты, которые не откатывают, и разные соединения на разных окружениях.
Грабля 1. Пустой или неправильный down()
Когда генерируете миграцию через yii migrate/create, Yii создаёт заготовку с методами safeUp() и safeDown(). В safeDown() по умолчанию стоит:
public function safeDown()
{
echo "m240101_120000_add_status_to_orders cannot be reverted.\n";
return false;
}
Если вы дописали safeUp(), но забыли про safeDown(), откат физически невозможен. На деве это терпимо — пересоздали базу. На проде вы упрётесь в стену: миграция накатилась, оказалась ошибочной, а откатить нельзя. Правило простое: каждое действие в safeUp() должно иметь зеркальное в safeDown().
public function safeUp()
{
// добавляем столбец статуса заказа
$this->addColumn('{{%orders}}', 'status', $this->string(20)->notNull()->defaultValue('new'));
}
public function safeDown()
{
$this->dropColumn('{{%orders}}', 'status');
}
Проверка: после yii migrate и последующего yii migrate/down 1 схема должна вернуться к исходной. Если down печатает «cannot be reverted» — вы забыли зеркало.
Грабля 2. safeUp против up: транзакции и DDL
Методы safeUp() / safeDown() оборачивают операции в транзакцию, чтобы при ошибке откатить всё разом. Но это работает не везде. В MySQL с движком InnoDB операции DDL1 (создание таблицы, ALTER TABLE) не транзакционны — они вызывают неявный коммит. Если миграция создаёт две таблицы, и вторая падает, первая останется в базе, а транзакция вокруг неё уже ничего не спасёт.
В MySQL надежда на «транзакция откатит все ALTER разом» ложная. safeUp даёт защиту только там, где СУБД умеет транзакционный DDL — например, в PostgreSQL. На MySQL считайте, что каждый ALTER необратим в рамках одной миграции.
Практический вывод: на MySQL держите одну миграцию — одно логически атомарное изменение. Если операция сложная и может упасть на середине, разбейте на отдельные миграции, чтобы точка отката была чёткой.
Грабля 3. Foreign key не создаётся или не удаляется
Внешние ключи требуют, чтобы движки и типы столбцов совпадали. Частый сценарий: базовая таблица создана с движком MyISAM (или без явного указания), а вы пытаетесь навесить FK — MySQL молча его не создаёт или падает с ошибкой errno 150.
Второй частый провал — порядок в safeDown(). Нельзя удалить таблицу, на которую ссылается живой внешний ключ. В откате сначала снимаете FK, потом дропаете таблицу:
public function safeUp()
{
$this->createTable('{{%order_items}}', [
'id' => $this->primaryKey(),
'order_id' => $this->integer()->notNull(),
], 'ENGINE=InnoDB');
// именуем ключ явно, иначе удалять придётся вслепую
$this->addForeignKey(
'fk-order_items-order_id',
'{{%order_items}}', 'order_id',
'{{%orders}}', 'id',
'CASCADE', 'CASCADE'
);
}
public function safeDown()
{
$this->dropForeignKey('fk-order_items-order_id', '{{%order_items}}');
$this->dropTable('{{%order_items}}');
}
Именуйте ключи и индексы сами. Если положиться на автогенерацию имени, в safeDown() вы не будете знать точное имя ключа, а на разных версиях СУБД оно может отличаться.
Грабля 4. Не то соединение и не та таблица истории
По умолчанию миграции работают с компонентом db и пишут историю в таблицу migration. Если в проекте несколько баз — например, отдельное соединение для аналитики — миграции для второй базы уедут не туда. Указывайте соединение и таблицу истории явно:
# миграции второй базы с отдельной таблицей истории
./yii migrate --db=dbAnalytics --migrationTable=migration_analytics --migrationPath=@app/migrations/analytics
Проверка: после команды в базе dbAnalytics должна появиться (или обновиться) таблица migration_analytics, а не запись в основной migration. Если запись ушла в основную базу — вы забыли флаг --db.
Начиная с новых версий Yii 2, вместо длинного набора флагов удобнее держать namespace-миграции и параметры в консольном конфиге через контроллер migrate — точные имена свойств контроллера сверьте в документации вашей версии, чтобы не гадать.
Грабля 5. Правки схемы руками в обход миграций
Кто-то зашёл в phpMyAdmin, добавил индекс, чтобы «быстро починить». Индекс работает, но в таблице migration о нём ни строки. Следующий разработчик накатывает миграцию, которая создаёт этот же индекс, — получает ошибку «duplicate key name». Или наоборот: миграция ждёт столбец, которого на его машине нет.
Лечение организационное, а не техническое: любое изменение схемы — только через миграцию. Ручную правку на проде фиксируйте отдельной миграцией задним числом (создайте её, впишите то же изменение как safeUp(), и отметьте применённой через yii migrate/mark), чтобы история совпала с реальностью.
Сравнение подходов к отмене изменений
| Ситуация | MySQL / InnoDB | PostgreSQL |
|---|---|---|
| ALTER TABLE в транзакции | Неявный коммит, откат не спасёт | Откатывается в транзакции |
| Несколько DDL в одной safeUp | Разбивайте на миграции | Можно держать вместе |
| Гарантия атомарности отката | Только вручную через safeDown | Транзакция safeUp/safeDown |
Если не получилось
- «cannot be reverted» при
migrate/down— вsafeDown()стоит заглушка сreturn false. Допишите зеркальные операции. - «Foreign key constraint is incorrectly formed» / errno 150 — не совпали типы столбцов или движки таблиц. Проверьте, что обе таблицы InnoDB и типы столбцов идентичны (включая знаковость и длину).
- «Duplicate key name» / «Duplicate column name» — изменение уже внесено руками. Сверьте реальную схему с тем, что делает миграция, и синхронизируйте историю через
migrate/mark. - «Table 'migration' doesn't exist» — первая миграция ещё не запускалась в этой базе или указано не то соединение. Yii создаёт таблицу истории при первом запуске; проверьте флаг
--db. - Откат «застрял» на середине — на MySQL часть DDL уже закоммичена. Досоздайте недостающие объекты вручную и приведите таблицу
migrationв согласованное состояние.
Что делать дальше
Перед выкладкой на прод прогоняйте связку migrate → migrate/down → migrate на копии продакшн-базы: если миграция не откатывается на реальных данных, вы узнаете об этом до релиза, а не во время инцидента. Держите бэкап перед каждым накатом на проде — миграция с необратимым DDL на MySQL не оставит вам второго шанса. И договоритесь в команде: схема меняется только через файлы миграций, коммит с изменением модели без миграции не проходит ревью.
Защитник прав потребителей: Досудебные претензии на возврат товара со ссылками на ЗоЗПП.
Спросить за 15 ₽Источники: Yii 2 Guide — Database Migration, Class yii\db\Migration — API Reference
Частые вопросы
У меня не Yii 2, а другая версия — приёмы те же?
Почему транзакция в safeUp не откатила ALTER TABLE?
Как безопасно удалить таблицу с внешним ключом?
Кто-то поправил схему руками на проде, как вернуть согласованность?
Что будет, если я запущу migrate на проде без бэкапа и он упадёт на середине?
Можно ли применять миграции только к одной из нескольких баз?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.