Сегодняшний день для меня ознаменовался эпичной битвой с django-evolution. С различными неудобствами этого приложения мы в разработке сначала Куда Все Идут, а потом Афиши сталкиваемся уже довольно давно. Сегодняшний же случай наконец заставил меня перебороть лень и изложить эти проблемы для публики.
Должен, тем не менее, обязательно сказать, что не считаю django-evolution плохим средством, потому что знаю, что во многих сценариях использования он вполне справляется со своими обязанностями. Однако архитектурные особенности нашего проекта и наши рабочие привычки, видимо, совмещаются с ним плохо. Поэтому принимайте текст как есть, без далеко идущих выводов, просто к сведению.
Очерёдность с syncdb
Одно из выбранных решений django-evolution — невмешательство в дела команды syncdb. То есть, django-evolution никогда не пытается выполнять SQL код для создания таблиц.
Первое неудобство в этом для нас — более сложный процесс составления миграционного SQL'а. Дело в том, что наши админы не шибко любят запускать изменения в базах всякими неконтролируемыми тулзами, а просят присылать им SQL-скрипты. Django-evolution умеет генерить SQL своих эволюций, а SQL для создания таблиц приходится вырезать руками из базы отдельно.
Но гораздо хуже другое. Представьте, что в процессе миграции базы с достаточно старой версии есть и новые модели, и эволюции, применяемые к этим моделям. Поскольку создавать таблицы надо отдельной командой, надо помнить, что этот SQL должен обязательно идти до эволюций. Несколько муторно. Теперь представьте, что среди эволюций есть переименование одной таблицы (соответствующее переименованию модели). С точки зрения syncdb таблицы для нового имени модели нет, и она её создаст. От этого эволюция переименования накроется, потому что таблица уже существует.
Приехали — есть эволюции, которые надо запускать до syncdb, а есть те, которые после. Соответственно, процесс превращается в совсем ручной cherry-picking.
Были ещё какие-то частные случаи, когда важен порядок применения syncdb и evolve, и в итоге, чтобы избавиться от этих проблем, мы стали вносить создания таблиц в эволюции вручную.
Написание симуляций
Кто не знает, в django-evolution есть интересная фича: она умеет не применять эволюции непосредственно к базе, а прогнать их сначала вхолостую ("simulate" в терминах приложения). Делается это не реально в БД, а спомощью выполнения специально написанных функций, которые меняют сигнатуру проекта — представление текущего вида моделей в памяти. Это примерно такой словарик:
{
'myapp': {
'MyModel1': {'fields': [ ... ], 'meta': [ ... ]},
'MyModel2': {'fields': [ ... ], 'meta': [ ... ]},
}
}
Идея состоит в том, что django-evolution берёт сохранённую сигнатуру из БД, которая соответствует предыдущему её состоянию, и протаскивает её через симуляционные функции всех неприменённых эволюций. Они меняют сигнатуру в памяти, и если в конце она соответствует тому, какая у вас модель в Питоне сейчас описана, django-evolution рапортует, что наличествующих эволюций достаточно для того, чтобы проапгрейдить базу.
Это хорошо работает в теории, если всю эту симуляцию делает за вас миграционное приложение. Однако на практике выходит так, что кроме обычных эволюций типа добавить поле/удалить поле у нас очень немало кастомных эволюций. Создания таблиц, переименования таблиц, разделения таблиц на несколько и т.п. Получается, что на каждый такой кастомный чих надо писать два кода: реальный SQL и симуляцию.
Хуже того, многие симуляции нужны только для того, чтобы evolve перестал жаловаться на несовпадение при симуляции, потому что в этом случае он отказывается что-либо делать вообще. Ни применить эволюции, ни SQL для них вывести. Нет у него ключика --force
...
В качестве грустного примера могу привести эволюцию "DeleteFieldWithoutSQL", которая нужна для того, чтобы симуляция показала, что поле из модели удаляется, но при этом чтобы оно не удалялось физически, потому что позднее мы перетаскиваем данные из него в другую таблицу, и только потом удаляем.
В итоге по факту оказывается, что ещё не было ни одного релиза у нас, когда бы мы не тратили немало времени на то, чтобы дописывать код только для того, чтобы evolve хотя бы просто запускался поверх довольно старой версии БД. То есть по очереди во время разработки всё вроде применилось, а вот скопом — нет.
Hint'ованные и сохранённые эволюции
Эволюции можно применять двояко. Можно попросить автоматически определить текущую разницу между кодом и базой (--hint
) и тут же её применить. А можно сохранить эволюцию в файл и зарегистрировать её в последовательности применения, чтобы она была применена при апгрейде другой БД.
Теперь представьте себе вполне обычный workflow:
- разработчик делает изменения в модели
- через
evolve --hint -x
отражает его в БД - тестирует, отлаживает
Теперь, чтобы это скоммитить, надо обязательно записать проведённые эволюции в виде файла, чтобы автоматически апгрейдить другие БД. А откуда вы теперь эти эволюции возьмёте? Всё ведь уже применено.
Казалось бы, можно ввести в процесс разработки правило, что никогда нельзя применять hint'ованные эволюции, а делать их только в виде файлов. Но во-первых, слишком много таких "гигиенических" правил сильно нагружают голову, а во-вторых это просто не работает. Потому что не родился ещё тот разработчик, который бы с первого раза писал правильный код, в том числе и при изменении схемы БД. Всегда нужно что-то переделать по результатам отладки.
В итоге это тоже вечная война: или копайся, в кишках таблиц django-evolution, вручную вычищая информацию о применённых эволюциях, или убирай вручную изменения в базе и делай новую эволюцию.
Наличие двух видов эволюций мне сейчас кажется, пожалуй, главным архитектурным изъяном django-evolution. Хотя может быть, мы просто не поняли, как этим пользоваться...
Ключик "всё хорошо"
Обратная ситуация тоже частенько случается: у вас есть некая база, в которой что-то переделано вручную, что-то откуда-то сдамплено/скопировано. Однако программист уверен, что она соответствует тому, что есть в моделях. А команда evolve — нет. Потому что в базе не записано, что были применены сохранённые эволюции, она пытается их симулировать и находит ошибки от повторного применения.
К сожалению, у команды evolve нет никакого ключика, чтобы сказать базе: "всё применено руками, просто запиши новое состояние и считай, что все эволюции применены". Да, считается, что при использовании django-evolution нельзя лезть в БД руками. Но я поверю в практичность такого подхода, когда оно само будет работать идеального. А пока реальность такова, что в базу приходится лазить руками.
Не фиксируются отдельные эволюции
Даже для многих админов БД является откровением, что PostgreSQL умеет делать DDL в транзакциях. Ни одна другая СУБД из набора Джанги этого не умеет, даже Oracle. И надо ж случиться такому совпадению, что ядрёныекорные разработчики Джанги предпочитают именно Postgres. Рассел (автор django-evolution), кажется, тоже его предпочитает. Никак иначе я не могу объяснить то, что применяемые эволюции фиксируются как выполненные только по успешному завершению всей последовательности эволюций.
Представьте себе теперь типичную ситуацию выезда в продакшн на MySQL-сервере какой-нибудь напряжённой итерации разработки. Применямых эволюций там — десяток. И на разработческой базе они прекрасно применялись. А вот на живы данных — упали. Где-то в середине. С Postgres'ом бы не было проблем, транзакция бы не скоммитилась. Но у нас вот часть изменений применилась, а часть нет. Какая именно часть применилась, django-evolution в БД не записал. И теперь, если начать миграцию заново, оно будет пытаться применять уже применённые вещи, и напарываться на всякие "already exists" для создаваемых полей и "not exists" для удаляемых.
Если бы это действительно происходило в продакшне, то да, вы в ж... живописно чудовищной ситуации. Поэтому мы обычно перед выкладкой закатываем свежую продакшн-базу на тестовый сервер и имитируем там весь процесс выкладки. И поэтому же наши админы предпочитают работать с простым набором SQL-команд, а не с излишне автоматическими тулзами.
Эволюции лежат в приложениях
Считается, что эволюции относятся только к моделям одного приложения. Однако бывают эволюции, когда надо перетащить модельку из одного приложения в другое, например после рефакторинга, в результате которого второе приложение и возникло. Непонятно, куда класть такую эволюцию.
Еще одна (совсем специфичная для нас) проблема возникает от того, что у нас два разных проекта живут на одной базе и используют одно общее приложение и свои собственные приложения, про которые другой проект не знает. Когда один проект применяет эволюции для себя, django-evolution записывает в базу сигнатуру этого проекта. В последствие, когда другому проекту надо применить эволюции своих приложений, он напарывается на сигнатуру, в которой нет его приложений вообще, зато есть какие-то левые другие.
Поэтому нам хочется иметь эволюции, относящиеся к какой-то конкретной базе, и лежащие где-то отдельно от структуры приложений.
Что вместо?
В связи с вышеизложенным мы, накушавшись, хотим попробовать что-то другое.
Видимо, это будут dmigrations. Миграции там хранятся в отдельной директории, применяются только из сохранённых файлов, и там есть ручной контроль — команда mark_as_applied
. Очень похоже, что их авторы исходили из очень практических use-case'ов и наступали на похожие с нашими грабли.
South решили не пробовать. Саша утверждает, что он "ещё хуже", чем django-evolution :-). А остальным разработчикам, включая меня, кажется, должно быть всё равно, потому что не работали ни с South, ни с dmigrations.
Посмотрим, что получится.
Комментарии: 19
А некоторые хвалят... :)
http://www.davidgrant.ca/south_amazing
Регулярно сталкиваемся с большиством этих проблем. Хочу поделиться парой используемых трюков:
Отмечаем все эволюции как примененные:
Если хотим отметить не все, то перед применением редактируем общий список эволюций и убираем те, которые не нужно считать примененными. После syncdb возвращаем список в исходное состояние.
Вместо
./manage.py evolve
используем свой скрипт, который сначала бэкапит базу, и только потом зовет evolve. Решаем таким образом проблему с транзакционностью и получаем способ откатиться к началу серии экспериментов с--hint --execute
Есть еще вот: http://code.google.com/p/deseb
Если
То зачем использовать django-evolution? Почему не писать этот sql руками?
Даже, если это менее приятно, чем писать эволюционный код, но зато на продакшн-сервере не возникает никаких проблем.
Иван, а не deseb не рассматривался как вариант? Просто я его использую, мне хватает, но проектик у меня, конечно, простой по сравнению с какой-нибудь Афишей. Вот и интересно, может слезание с него даст какие-то неизведанные преимущества :)
А мы просто пишем запросы руками и сохраняем в спец. каталог. А оттуда скрипт их выполняет.
Вот тут написано следующее:
Я несколько месяцев пользую South (в текущем проекте что-то около 60-70 миграций). Вполне доволен.
Проблем с syncdb нет. Если приложение имеет миграции, то syncdb для него не отрабатывает. При запуске ./manage.py syncdb пишется что-то вроде:
Про симуляции не совсем понял понял. У South у команды migrate есть ключик --db-dry-run - он проверяет, нормально ли отработают миграции или нет.
Хинтованные/сохраненные эволюции. В South все эволюции только из файлов. При этом есть как upgrade схемы (метод forwards), так и downgrade (метод backwards). То есть если разработчику не понравилось изменение, то он делает migrate до предыдущей версии, переделывает migration и накатывает его заново.
Ключик "все хорошо" в South - это --fake. При этом миграции не выполняются, а просто в базу записывается текущая версия схемы.
South фиксирует отдельные миграции. Если при какой-то миграции случился облом, то при следующем ./manage.py migrate миграция начнется с обломавшегося файла.
А мы используем south. Вроде безболезненно. Спроси у arikon@
Мы используем South и пока довольны.
Всем: про South понял, может тогда на него раньше посмотрим.
Сергей:
А мы и пишем руками. То, что эволюции умеют подсказывать SQL'ный код — это всего лишь дополнительное удобство. Вся суть таких систем в том, чтобы они хранили состояние базы и набор изменений, позволяя любому разработчику в команде довести любую базу из своего текущего состояния в современное.
Про deseb... Не хочу задеть его авторов лично, но его архитектура убога. Помимо того, что зашивание информации о миграции в модель просто некрасиво, это сильно ограничивает всю идею: невозможно в models.py таскать с собой неограниченное количество неприменённых миграций. Плюс, я не очень понимаю, как с помощью хинта "aka" можно выразить что-то больше, чем добавление/переименование одного поля или одной модели, причём только на одну итерацию. Наверное это работает только в случае, если один разработчик добавил поле в модель на своём сервере и тут же пошёл сделал это на продакшне. Если у вас несколько разработчиков или изменения выкладываются в продакшн не сразу — это не работает.
Иван Сагалаев:
А чем тогда не устраивает маленький скриптик на питоне, который бы прогонял все ваши миграции (написанные на sql) из любого состояния до up to date.
А если хочется еще и rollback'ов, то тоже писать их руками с учетом хинтов от south или другого инструмента.
Так все эти тулзы и есть в корне такой небольшой питоний скриптик :-). То, что он на самом деле большой, меня не очень беспокоит. Мне хочется, чтобы их поддерживал кто-то другой :-).
С South наткнулся на неприятное свойство, что оно при создании первой миграции делает порядок полей в таблицах совершенно произвольным. Видимо, оно промежуточно в
dict
держит данные. Приходится после созданий первой миграции лезть и руками править то, что оно там насоздавало, чтобы все чинно было.С одной стороны, вроде бы и мелочи и плевать, практически все все равно через ORM делается, а с другой как-то некрасиво совершенно.
К слову, упомянутый маленький питонский скриптик, который прогоняет все SQL миграции до последнего состояния написал мой коллега, находится скриптик здесь: https://github.com/kmerenkov/dbup/tree
Правда, поддерживать мы его [скрипт] больше не будем, потому что у нас оказалось всё намного проще, комитим один update.sql, обновляем create.sql и выкатываем.
south может генерировать "начальную" миграцию, в которой создаються таблицы (т.е. для пустой базы)
И на чем остановились?
Кто на чём. Кто south использует, а кто просто руками SQL пишет. На практике последнее оказывается иногда проще, чем попытка приучить себя жить со своенравной системой.