Миграции Yii2 для production: как писать безопаснее
Миграция Yii2 на production — это не просто способ изменить структуру базы. Это часть релиза, которая может заблокировать таблицу, сломать данные, не откатиться или выполниться повторно после частичного сбоя. Поэтому миграции нужно писать так, чтобы они были предсказуемыми.
Главная цель production-миграции — изменить базу безопасно, понятно и с контролируемым откатом.
safeUp и safeDown
Yii2 поддерживает safeUp() и safeDown(), которые выполняются в транзакции, если СУБД и операции это позволяют.
public function safeUp()
{
$this->addColumn('{{%orders}}', 'external_id', $this->string(64)->null());
}
public function safeDown()
{
$this->dropColumn('{{%orders}}', 'external_id');
}
Но не все операции безопасно откатываются транзакцией. Например, изменение большой таблицы или создание индекса может вести себя по-разному в зависимости от MySQL-версии и движка.
Проверять существование объектов
Если миграция может запускаться на разных стендах, полезно проверять наличие колонок, индексов или таблиц.
$table = $this->db->schema->getTableSchema('{{%orders}}');
if (!isset($table->columns['external_id'])) {
$this->addColumn('{{%orders}}', 'external_id', $this->string(64)->null());
}
Это не заменяет нормальный порядок миграций, но снижает риск на проектах с долгой историей.
Индексы на больших таблицах
Создание индекса на большой таблице может занять время и создать нагрузку. Перед добавлением индекса нужно понимать размер таблицы и активность записи.
SELECT COUNT(*) FROM orders;
Индекс должен иметь понятное имя:
$this->createIndex(
'idx_orders_external_id',
'{{%orders}}',
'external_id'
);
Для уникального индекса сначала нужно проверить дубли. Иначе миграция упадёт в середине релиза.
SELECT external_id, COUNT(*)
FROM orders
WHERE external_id IS NOT NULL
GROUP BY external_id
HAVING COUNT(*) > 1;
Данные и структура отдельно
Если миграция одновременно меняет структуру, пересчитывает миллион строк и добавляет индекс, её сложнее откатывать и диагностировать. Часто лучше разделить на несколько миграций:
- добавить nullable-колонку;
- заполнить данные пакетами;
- проверить результат;
- добавить индекс или ограничение;
- переключить код на новое поле.
Batch-обработка данных
Если нужно обновить много строк, не стоит делать всё одним огромным UPDATE без оценки. Для сложной логики лучше обрабатывать пакетами.
$lastId = 0;
while (true) {
$rows = (new \yii\db\Query())
->select(['id', 'old_value'])
->from('{{%items}}')
->where(['>', 'id', $lastId])
->orderBy(['id' => SORT_ASC])
->limit(1000)
->all();
if (empty($rows)) {
break;
}
foreach ($rows as $row) {
$lastId = (int)$row['id'];
// обновление
}
}
Для очень больших объёмов иногда лучше вынести заполнение данных в отдельную консольную команду, а не держать релизную миграцию слишком долго.
Не использовать ActiveRecord в миграциях
ActiveRecord зависит от текущего состояния моделей. Через год модель изменится, а старая миграция может начать работать иначе или сломаться. В миграциях надёжнее использовать Query, createCommand, insert, batchInsert и raw SQL.
$this->batchInsert('{{%settings}}', ['code', 'value'], [
['site_name', 'Example'],
['items_per_page', '20'],
]);
Откат не всегда должен удалять данные
safeDown должен быть честным. Если миграция необратимо преобразовала данные, лучше вернуть false, чем создавать видимость отката.
public function safeDown()
{
echo "m260618_000014 cannot be reverted safely.\n";
return false;
}
Если откат возможен, его нужно проверить на тестовой копии.
Чек-лист
- Понимать размер таблиц, которые меняются.
- Проверять дубли перед уникальными индексами.
- Разделять структуру и тяжёлую обработку данных.
- Использовать понятные имена индексов.
- Не использовать ActiveRecord в миграциях.
- Писать честный safeDown.
- Проверять миграцию на копии production-базы.
- Иметь бэкап и план отката перед релизом.
Хорошая миграция не удивляет. Она делает конкретное изменение, проверяет риски, не зависит от текущих моделей и понятно откатывается или честно сообщает, что безопасного отката нет.
Комментарии (0)
Пока нет комментариев. Будьте первым!