Приватні проблеми використання 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" у термінах додатку). Робиться це не реально в БД, а за допомогою виконання спеціально написаних функцій, які змінюють сигнатуру проекту – представлення поточного виду моделей у пам'яті. Це приблизно такий словничок:
Ідея полягає в тому, що django-evolution бере збережену сигнатуру з БД, яка відповідає попередньому стану, і протягує її через симуляційні функції всіх незастосованих еволюцій. Вони змінюють сигнатуру в пам'яті, і якщо в кінці вона відповідає тому, яка у вас модель в Пітоні зараз описана, django-evolution рапортує, що еволюцій достатньо для того, щоб проапгрейдить базу.
Це добре працює в теорії, якщо всю цю симуляцію робить за вас міграційний додаток. Однак на практиці виходить так, що крім звичайних еволюцій типу додати поле/видалити поле у нас дуже багато кастомних еволюцій. Створення таблиць, перейменування таблиць, поділу таблиць кілька і т.п. Виходить, що на кожен такий кастомний чих треба писати два коди: реальний SQL і симуляцію.
Гірше того, багато симуляцій потрібні тільки для того, щоб evolve перестав скаржитися на розбіжність при симуляції, тому що в цьому випадку він відмовляється щось робити взагалі. Ні застосувати еволюції, ні SQL їм вивести. Немає у нього ключика - force.
Як сумний приклад можу навести еволюцію "DeleteFieldWithoutSQL", яка потрібна для того, щоб симуляція показала, що поле з моделі видаляється, але при цьому щоб воно не видалялося фізично, тому що пізніше ми перетягуємо дані з нього в іншу таблицю, і тільки потім видаляємо.
У результаті за фактом виявляється, що ще не було жодного релізу у нас, коли б ми не витрачали чимало часу на те, щоб дописувати код тільки для того, щоб evolve хоча б просто запускався поверх старої версії БД. Тобто по черзі під час розробки все начебто застосовалось, а ось скопом - ні.
Hint'овані та збережені еволюції
Еволюції можна застосовувати подвійно. Можна попросити автоматично визначити поточну різницю між кодом і базою ( --hint ) і відразу застосувати. А можна зберегти еволюцію у файл та зареєструвати її в послідовності застосування, щоб вона була застосована при апгрейді іншої БД.
Тепер уявіть собі цілком звичайний workflow:
- розробник робить зміни у моделі
- через evolve --hint -x відбиває його в БД
- тестує, налагоджує
Тепер, щоб це з'єднати, треба обов'язково записати проведені еволюції у вигляді файлу, щоб автоматично апгрейдити інші БД. А де ви тепер ці еволюції візьмете? Адже все застосовано.
Здавалося б, можна ввести в процес розробки правило, що ніколи не можна застосовувати hint'овані еволюції, а робити їх лише у вигляді файлів. Але по-перше, дуже багато таких "гігієнічних" правил сильно навантажують голову, а по-друге, це просто не працює. Тому що не народився ще той розробник, який з першого разу писав правильний код, у тому числі і при зміні схеми БД. Завжди потрібно щось переробити за наслідками налагодження.
У результаті це теж вічна війна: або копайся, в кишках таблиць django-evolution, вручну вичищаючи інформацію про застосовані еволюції, або прибирай вручну зміни в базі і роби нову еволюцію.
Наявність двох видів еволюцій мені зараз здається, мабуть, головною архітектурною вадоюdjango-еволюція. Хоча, можливо, ми просто не зрозуміли, як цим користуватися.
Ключик "все добре"
Зворотна ситуація теж часто трапляється: у вас є якась основа, в якій щось перероблено вручну, щось звідкись здамплено/скопійовано. Однак програміст упевнений, що вона відповідає тому, що є у моделях. А команда evolve – ні. Тому що в основі не записано, що були використані збережені еволюції, вона намагається їх симулювати і знаходить помилки від повторного застосування.
На жаль, команда evolve не має жодного ключика, щоб сказати базі: "все застосовано руками, просто запиши новий стан і вважай, що всі еволюції застосовані". Так, вважається, що з використанням django-evolution не можна лізти в БД руками. Але я повірю в практичність такого підходу, коли він сам працюватиме ідеального. А поки що реальність така, що в базу доводиться лазити руками.
Не фіксуються окремі еволюції
Уявіть собі тепер типову ситуацію виїзду в продакшн на MySQL-сервері якоїсь напруженої ітерації розробки. Застосовуваних еволюцій там — десяток. І на базі розробок вони чудово застосовувалися. А ось на живі дані впали. Десь у середині. З Postgres'ом не було б проблем, транзакція б не знітилася. Але у нас ось частина змін застосовалась, а частина ні. Яка саме частина застосувалася, django-evolution у БД не записав. І тепер, якщо почати міграцію заново, воно намагатиметься застосовувати вже застосовані речі, і напарюватися на всякі "already exists" для полів, що створюються, і "not exists" для видалених.
Якби це справді відбувалося у продакшні, то так, ви ж. мальовничо жахливої ситуації. Тому ми зазвичай перед викладкою закочуємо свіжу продакшн-базу на тестовий сервер та імітуємо там весь процесвикладки. І тому ж наші адміни воліють працювати з простим набором SQL-команд, а не надмірно автоматичними тулзами.
Еволюції лежать у додатках
Вважається, що еволюції відносяться лише до моделей одного додатка. Однак бувають еволюції, коли треба перетягнути модельку з одного додатка до іншого, наприклад після рефакторингу, в результаті якого другий додаток і виник. Незрозуміло, куди класти таку еволюцію.
Ще одна (зовсім специфічна для нас) проблема виникає від того, що у нас два різні проекти живуть на одній базі і використовують один спільний додаток та свої власні додатки, про які інший проект не знає. Коли один проект застосовує для себе еволюції, django-evolution записує в основу сигнатуру цього проекту. В наслідок, коли іншому проекту треба застосувати еволюції своїх додатків, він напарюється на сигнатуру, де немає його додатків взагалі, зате є якісь ліві інші.
Тому нам хочеться мати еволюції, які стосуються якоїсь конкретної бази, і десь окремо від структури додатків.
Що замість?
У зв'язку з вищевикладеним ми, скуштувавши, хочемо спробувати щось інше.
South вирішили не пробувати. Саша стверджує, що він "ще гірший", ніж django-evolution :-). А решті розробників, включаючи мене, здається, має бути все одно, тому що не працювали ні з South, ні з dmigrations.
Подивимось що вийде.
Коментарі: 19
Регулярно стикаємося з більшістю цих проблем. Хочу поділитися парою трюків, що використовуються:
Відзначаємо всі еволюції як застосовані:
Якщо хочемо відзначити не все, то перед застосуванням редагуємо загальний список еволюцій та прибираємо ті, які не слід вважати застосованими. Після syncdb повертаємосписок у вихідний стан.
Замість ./manage.py evolve використовуємо свій скрипт, який спочатку бекапіт базу, і тільки потім кличе evolve. Вирішуємо таким чином проблему з транзакційністю і отримуємо спосіб відкотитися до початку серії експериментів з --hint --execute
Справа в тому, що наші адміни не дуже люблять запускати зміни в базах будь-якими неконтрольованими тулзами, а просять надсилати їм SQL-скрипти.
То навіщо використати django-evolution? Чому не писати цей SQL руками?
Навіть якщо це менш приємно, ніж писати еволюційний код, зате на продакшн-сервері не виникає жодних проблем.
Іван, а не desebe не розглядався як варіант? Просто я його використовую, мені вистачає, але проектик у мене, звичайно, простий у порівнянні з якоюсь Афішею. Ось і цікаво, може злізання з нього дасть якісь незвідані переваги :)
А ми просто пишемо запити руками та зберігаємо у спец. каталог. А звідти їх виконує скрипт.
I'm member of the team that developed dmigrations - but I would wholeheartedly recommend South. Це дуже багато матерії, є під активним розвитком, і має деякі кільки риси як ORM freezing (якщо ви намагаєтеся використати ORM код в мігрантах, то змінюють ваші моделі, ви в для світового світу).
Я кілька місяців користуюсь 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-скрипти.
То навіщо використати django-evolution? Чому не писати цей SQL руками?
А ми й пишемо руками. Те, що еволюції вміють підказувати SQL'ний код — це лише додаткова зручність. Вся суть таких систем у тому, щоб вони зберігали стан бази та набір змін, дозволяючи будь-якому розробнику в команді довести будь-яку базу зі свого поточного стану до сучасного.
А ми й пишемо руками. Те, що еволюції вміють підказувати SQL'ний код — це лише додаткова зручність. Вся суть таких систем у тому, щоб вони зберігали стан бази та набір змін, дозволяючи будь-якому розробнику в команді довести будь-яку базу зі свого поточного стану до сучасного.
А чим тоді не влаштовує маленький скриптик на пітоні, який проганяв би всі ваші міграції (написані на sql) з будь-якого стану до up to date.
А якщо хочеться ще й rollback'ів, то теж писати їх руками з урахуванням хінтів від south чи іншогоінструмент.
А чим тоді не влаштовує маленький скриптик на пітоні, який проганяв би всі ваші міграції (написані на sql) з будь-якого стану до up to date.
Так усі ці тулзи і є докорінно такий невеликий питоний скриптик :-). Те, що він справді великий, мене не дуже турбує. Мені хочеться, щоб їх підтримував хтось інший :-).
З South натрапив на неприємну властивість, що вона при створенні першої міграції робить порядок полів у таблицях довільним. Мабуть, воно проміжно у dict тримає дані. Доводиться після створення першої міграції лізти і руками правити те, що воно там настворювало, щоб усе чинно було.
З одного боку, начебто і дрібниці і начхати, практично все одно через ORM робиться, а з іншого якось некрасиво зовсім.
До речі, згаданий маленький пітонський скриптик, який проганяє всі SQL міграції до останнього стану, написав мій колега, знаходиться скриптик тут: https://github.com/kmerenkov/dbup/tree
Правда, підтримувати ми його [скрипт] більше не будемо, тому що у нас виявилося все набагато простіше, комітуємо один update.sql, оновлюємо create.sql і викочуємо.
south може генерувати "початкову" міграцію, в якій створюються таблиці (тобто для порожньої бази)
І на чому зупинились?
Хто на чому? Хтось south використовує, а хтось просто руками SQL пише. Насправді останнє виявляється іноді простіше, ніж спроба привчити себе жити зі норовливою системою.