Pull to refresh

Doctrine: Опыт работы с миграциями в symfony

Symfony
Для тех, кто не в курсе, миграции — это способ внесения изменений в структуру БД.
Управлять изменениями можно по-разному, но все сводится к работе инструкциями для изменения стуктуры.

Почему миграции это делают наилучшим способом:
1. Автоматизация. Вы можете хранить инструкции в sql-файликах, накатывать их при необходимости. Но это становится дико неудобно, когда встает вопрос о переключении между разными ревизиями (версиями БД), для командной разработки, когда всем разработчикам надо накатить изменения, для развертывания тестового окружения.
2. Rollback (как продолжение первого пункта). Мы можем откатить любую миграцию и получить версию БД на любой момент. Чем это удобно, см. ниже.
3. Идентичность DEV и PROD версий БД. Это очень важно, по крайней мере для меня, быть уверенным в том, что версии DEV, PROD и TEST абсолютно одинаковы. Да, этого можно добиться и другими способами. Но когда именно миграции являются носителями информации о структуре БД, вместе с автоматизацией решать эту задачу становится намного удобнее и проще.

Не буду описывать базовые вещи, можно посмотреть:

Начало работы над проектом


Когда мы начинаем новый проект (и базы у нас еще нет), в процессе разработке я предпочитаю создавать таблицы с помощью миграций. С одной стороны, это можно делать немного проще — создавать базу из схемы. И, соответственно, выкатывать первый релиз на PROD аналогичным способом. Но, с другой стороны, работа на первом релизе не останавливается и обязательно возникает момент, когда необходимо внести новые изменения в структуру БД. В этом случае нам ОБЯЗАТЕЛЬНО надо будет зафиксировать первоначальную структуру в миграциях (см. ниже). Поэтому, чем переключаться между двумя подходами, я выбрал один.

Дальше, если у нас уже есть база данных (унаследованная или разрабатывалась без миграций), мы фиксируем ее структуру в миграциях.
Если мы этого не сделаем и напишем первую миграцию для уже существующей БД, в будущем мы получим много проблем, поверьте мне. Это и неприятная проблема с разворачиванием проекта, с созданием чистой базы для тестов, и перелючением между ревизиями.
В общем, ни в коем случае нельзя смешивать 2 подхода: управление структурой через миграции и другие варианты.

Если база унаследована, то, с большой вероятностью, доктрина не сможет сгенерировать схему и миграции для создания идентичной БД. В декларациях таблиц могут использоваться специфичные для движка инструкции, могут встречаться зарезервированные слова.

Поэтому, чтобы зафиксировать идентичное состояние, я пишу первую миграцию, которая содержит raw-дамп структуры. Что-то вроде: $this->rawQuery(«CREATE TABLE ...»)
Этим я добиваюсь 100% идентичности и всегда уверен в том, что при накатывании новой миграции версии БД на DEV, PROD и TEST будут одинаковы, что тесты будут работать именно с той структурой, которая будет выкачена на PROD.
Я кладу все инструкции в одну миграцию, чтобы отделить ее от всех остальных и не плодить кучу файлов, как это делает доктрина при экспорте.
В итоге получается одна простая отправная точка, которая учитывает всё.

Как писать миграции


Миграции должны быть атомарными
Одна правка — одна миграция. Без фанатизма, конечно.
Почему? Если мы написали одну большую миграцию (таблицы, ключи, правки данных), и она у нас вдруг падает (а это легко, например создание FK на ключах с разными интами в унаследованной БД или создание FK, когда нарушена ссылочная целостность). Тогда у нас получится ситуация, когда часть изменений будет сделана, а часть нет. Какая именно часть — надо искать. Откатиться обратно мы не можем, поскольку номер миграции не изменился. Поэтому, потребуется искать где упало, что уже изменено, а что нет, откатывать это все ручками. Или можно изменить вручную номер миграции, если ролбек будет в состоянии откатить все назад. Еще можно, если нам позволяют условия, грохнуть БД и накатить миграции до ошибочной и уже там разбираться, что упало.
В общем, чем рассуждать о вероятностях и возможных подходах, делаем атомарные миграции и горя не знаем. Если что-то упало, то оно быстро диагностируется и исправляется без танцев с бубном.

Названия
В названии файла и класса миграции я указываю номер версии, модель, действие и описание:
    001_Article_CreateTable.php
    002_Article_AddColumn_AuthorId.php
    003_Article_AddFk_Authors.php
    004_Article_UpdateColumn_Title.php
    005_Article_DropTable.php
        класс:
    class Migration001_Article_CreateTable
Такой подход позволяет быстро найти/отфильтровать все миграции для заданной модели.
Удобно находить похожую миграцию, чтобы скопипастить ее для создания новой аналогичной миграции.

Я использую порядковые номера. Доктрине все равно, как мы называем миграции.
Генератор миграций подставляет timestamp, например. Главное — это порядок по алфавиту.
Я использую порядковые номера, чтобы они соответствовали номеру версии БД.
Потому что, если вы напишете 2 миграции с именами 1 и 100, то номер версии БД будет 2.
Я не хочу путаться.

Правда такой подход создает определенные неудобства при одновременной работе с миграциями в разных ветках. Если у нас есть 3 миграции, и 2 разработчика добавляют по одной в своей ветке, то при слиянии у нас будет 2 миграции с названием «4».
Пока у меня есть только 1 решение:
* При слиянии веток, переименовывать миграции отдельным коммитом, + для чистоты репо, объединить правки по конкретными миграциям в один коммит.
Поэтому я всегда рекомендую каждую миграцию оформлять отдельным коммитом,
чтобы потом можно было объединить все коммиты для одной миграции (в рамках решения одной задачи)
* Также, миграциям можно давать временные имена с заведомо большими индексами, учитывая, что они будут переименованы при слиянии.
P.s. У меня ветки короткие и сливаю я их ребейзом (git help rebase)

Migrate()
Если есть возможность, используем короткую запись вместе с migrate(). Это удобно и надежно — никто не забудет написать down() и не сделает там ошибку.

Rollback
Я всегда оставляю возможность откатить миграцию. Даже, если это «необратимая» миграция. У меня никогда еще не доходило до отката на PRDO (или я не доводил), но откаты, в первую очередь, важны для переключения между ревизиями.
Теоретически, когда база пустая, можно переключиться на любую ревизию и поднять текущую структуру из миграций. Но бывает, что в базе для разработки, есть ценные данные, которые не поднять из фикстур. В этом случае ролбек незаменим. А возиться с дампом каждый раз мне не хочется, особенно когда я по 10 раз на дню переключаюсь между разными ветками с разной структурой БД.
Поэтому у меня нет необратимых миграций. Если я удаляю таблицу или столбец, в ролбеке я ее создаю. В данном случае удаленные данные уже не важны, главное — не нарушать цепочку миграций, так чтобы всегда можно было отойти к самому началу и вернуться в исходную точку.
Поэтому я всегда пишу и тестирую откаты (--up; --down; --up). И бывает, что падают.

Миграции данных
Под миграциями данных я подразумеваю изменения не связанные со структурой БД. Т.е. добавить пару строк в справочные таблицы, удалить строки, обновить информацию после изменения структуры.

Сначала я начал писать эти изменения в рамках обычных миграций и напоролся на те же грабли, что и Ryan Weaver (см. ссылку в конце). Ели при накатывании с 50 на 100 миграцию, в 51 миграции используются модели, которые уже удалены или изменены (т.е. в текущей ревизии уже другое состояние), тогда миграция падает. И чтобы ее накатить, надо откатится на нужную ревизию, пересобрать модели, и тогда она пройдет.
Все это мне показалось неправильным и я решил отказаться изменений данных в рамках миграций. Решил, что миграции — это только изменение схемы БД и не больше. Я думал, что миграции данных нужны только на PROD, где есть данные, а тестам и разработчикам это не нужно. А фикстуры-справочники можно и через yml заливать.
Но как показала практика, это оказалось неудобным. Миграции данных тоже надо где-то фиксировать, накатывать ручками. Разработчикам оказалось тоже неудобно работать.

В итоге, я пришел к выводу, что миграции это одна удобная точка входа для управления структурой и данными БД. Особенно при командной разработке.
Поэтому, теперь миграции данных я пишу в обычных миграциях в pre/post хуках и главное — на чистом SQL, чтобы не использовать модели, которые зависят от текущей ревизии.
Плюс, получилось очень удобно хранить/контролировать фикстуры-справочники в миграциях.

Запуск миграций


Накатываем миграции по одной
Вообще, доктрина позволяет указать номер версии БД, до которой надо обновиться. Если миграции проверенные и вы уже протоптали дорожку, то ничего страшного в этом нет. А вот если они еще не обкатаны или вы накатываете на PROD, тогда их надо запускать по одной:
    ./symfony doctrine:migrate --up
А все потому, что когда вы обновляетесь одним махом, например со 2 по 10 версию, и у вас падает какая-то миграция (причем неизвестно какая), то номер версии не обновится и останется 2, хотя реальный номер может быть 5. И тогда приходится находить упавшую миграцию, править номер версии в БД, чтобы откатиться и чинить миграцию.
И вам очень не повезло, если упавшая миграция была не атомарная, а в базе есть ценные данные, которые нельзя удалить.
Поэтому, на PROD я всегда накатываю по одной с замиранием сердца. И еще предварительно тестирую на аналогичной копии БД.

Тесты
Для запуска полного набора тестов я созданю чистую БД из миграций:
    $task = new sfDoctrineBuildTask(new sfEventDispatcher, new sfFormatter);
    $task->run($args = array(), $options = array(
        'env' => 'test',
        'no-confirmation' => true,
        'db' => true,
        'and-migrate' => true,
    ));
Этим я добиваюсь 100% идентичности TEST и PROD. Плюс, косвенно тестирую работу приложения с учетом новых миграций.

Прочее


Обнуление миграций
Меня заинтересовала эта идея — время от времени удалять все миграции, большая часть которых уже стала избыточной и требует много времени для создания тестового окружения. Можно снова написать первую миграцию для начального импорта и изменить номер миграции в БД.
Правда, пока еще не пробовал и не знаю как это отразится работе с разными ревизиями проекта.


Аналогичные мысли по теме: Ryan Weaver
http://www.slideshare.net/weaverryan/the-art-of-doctrine-migrations
Tags:doctrinemigrationsymfony
Hubs: Symfony
Total votes 19: ↑18 and ↓1+17
Views28K
Comments Comments 27

Popular right now

Top of the last 24 hours