При знайомстві фреймворком Django багато хто схвально оцінює механізм міграцій. З його допомогою можна синхронізувати код у моделях Django з базою даних, яка під’єднана до вебзастосунку. При цьому міграції робляться автоматично, що значно полегшує роботу. Але чи так все гарно, як виглядає на перший погляд? З власного досвіду скажу, що на практиці з міграціями може бути чимало проблем.
Мене звати Михайло Сердюк, я Backend Developer в NIX та спікер IT-конференції NIX MultiConf. У цій статті я розповім, як вирішити проблеми, які можуть виникнути під час міграцій в Django. Поїхали!
Зміст
1. Що таке міграції
2. Навіщо розбиратися з міграціями
3. Як замінити міграцію
4. Складні випадки при міграціях
5. Дата-міграції
6. Скасування дата-міграції
7. Повторний запуск дата-міграцій
8. Фейк-міграції
9. Squashing migrations
10. Замість висновку
Що таке міграції
Для початку — трохи теорії. Припустимо, що в Django-застосунку в файлі моделей ми вказали певний клас, щоб описати, як має виглядати наша таблиця з даними:
У такому випадку зі створенням міграцій все було б добре. Але уявімо, що в якийсь момент з’явилась необхідність доповнити модель.
Наприклад, в Question
треба додати поле для опису title
питання. Може здатися, що достатньо просто вписати його, але насправді цього замало. Щоб Django сприйняв ці зміни і заніс їх до бази даних, модель потрібно промігрувати до Django. У наведеному нижче прикладі я навмисно задав для поля title
параметр blank=True
. Трохи далі по тексту поясню це:
Отже, ми змінили базу даних. Тепер потрібно створити міграцію. Для цього прописуємо команду manage.py makemigrations
. Після цього Django автоматично створить файл із міграціями. Ці файли створюються в хронологічному порядку, за нумерацією. Коли ми перший раз запускаємо застосунок, з’являється initial-міграція, а після неї — наступні. В нашому випадку це друга міграція з прописаними в моделі змінами.
Однак у нас з’явилися лише файли, які порівняли стан файлу з моделями застосунку. Для цього Django проаналізував файли міграції, створені до цього, з класами і моделями в файлі model.py.
Різниця між ними і стала основою файлу міграції. Але для застосування цього файлу в базі даних потрібно запустити ще одну команду — manage.py migrate
.
Після цього в консолі з’явиться вивід — міграції застосовані. При першому запуску застосунку цей вивід дуже масивний, адже в Django є багато вбудованих моделей, які першочергово застосовуються при міграції:
Варто розібратися, що являє собою сам файл із міграцією:
- По-перше, в ньому вказуються залежності, з яких він створений. Для цього робиться посилання з бази даних на останню застосовану міграцію.
- По-друге, в файлі вказуються потрібні зміни в базі даних. У нашому випадку це створення в моделі
question
нового поля з назвоюtitle
і параметри для нього:
Навіщо розбиратися з міграціями
Може здатися, що тут достатньо оперувати лише двома командами: makemigrations
та migration
. На невеликих проєктах це саме так і відбувається: пишете та застосовуєте міграції і додаєте код для оперування даними. Однак часто замовник формує задачу дуже абстрактно.
Поясню через аналогію. Клієнту потрібен транспорт, на якому він зможе дістатися з пункту А до пункту Б і перевезти вантаж. Виконавець, як раціональна людина, піде шляхом найменшого спротиву та зробить велосипед. Це транспорт? Так. Ним можна дістатися з пункту А до пункту Б? Можна. Він підходить для перевезення вантажу? Безумовно. І якщо повернутися до наших з вами завдань, то подібна ситуація може призвести до схеми бази даних, яка наведена на цій ілюстрації:
Це приклад першої реалізації бази даних, взятий з реальної практики. Тут були великі моделі, у них були властивості, пов’язані з основними моделями. Але замовник сказав, що має декілька зауважень.
Якщо повернутися до нашої аналогії, ситуація виглядала приблизно так:
- по-перше, відстань від пункту А до пункту Б велика, тому потрібен більш швидкий транспортний засіб, аніж велосипед;
- по-друге, на шляху чимало підйомів та спусків, тому замість педалей краще встановити двигун;
- по-третє, використання транспорту планується впродовж року, тому потрібна кабіна з обігрівачем і кондиціонером;
- та й взагалі вантажі великі, і багажника на велосипеді замало.
Після зауважень розробник вносить зміни до структури бази даних, і згодом проєкт трансформується у дещо інше. Результат змін ви можете побачити на наступній ілюстрації. До речі, ця схема — ще не кінцевий варіант, а десь 10% від реальної структури:
Цей приклад каже нам: постійні зміни бази даних — це нормально. Бо відразу прорахувати можливі ризики складно. Задача може коригуватися декілька разів. Та й, відверто кажучи, інколи сам замовник не може чітко описати потрібний йому продукт.
Саме тому розробнику дуже часто потрібно виправляти певні моменти в схемі бази даних. Для цього він має відкотити у змінах міграції все назад.
Таке може статися в декількох випадках. На щастя, є дієві варіанти, як із цим впоратися.
Як замінити міграцію
Перший і найбільш простий сценарій — коли ми створили файл, але не запустили команду migrate
. В такому разі зміни до бази даних не були застосовані. Тому можна просто видалити останні файли з міграціями, зробити зміни в моделях, створити нову міграцію та застосувати її.
Трохи складніша ситуація — коли ми і запускали команду migrate
, і застосували зміни з міграціями в базу даних. Для вирішення такої проблеми потрібно зрозуміти, чи важлива база даних. Якщо нею можна знехтувати, видаляйте всі міграції і базу даних, створюйте нові міграції з потрібними змінами і мігруйте в нову БД. Але якщо база даних важлива, тоді варто зробити відкат до попередніх міграцій. Для цього вводимо команду manage.py show migration
.
Далі побачимо застосовані в базу даних міграції. В моєму випадку попередньою була 0010_previous_migration
. Щоб до неї відкотитися, ми прописуємо команду:
manage.py migrate my_app 0010_previous_migration
Зазначаємо тут назви нашого застосунку і бажаної міграції. Після відкату залишається видалити непотрібну міграцію та зробити зміни в моделях, а потім заново промігрувати.
Крім цього, можна відкотитися і до нульової міграції — зробленої на початку створення застосунку. Для цього використовується команда manage.py migrate my_app zero
.
Складні випадки при міграціях
Впевнений, що більшість із вас з описаними прикладами вже так чи інакше стикалися. Але що робити в більш складних ситуаціях? Спробуємо розібрати найбільш поширені варіанти подібних проблем:
Уявімо, що в моделі question
із полем question_text
необхідно додати title
з описом, якої довжини мають бути дані в цьому полі.
Коли ми почнемо створювати міграції, Django відразу нам вкаже: у полі не прописано значення за замовчуванням. І хоча для нових полів це поле буде заповнюватись, Django повторюватиме: його треба заповнити.
Є декілька шляхів вирішення цієї проблеми. Розглянемо їх.
- Задати значення за замовчуванням
Таблиця на ілюстрації показує суть ситуації: ми створили поле title
, яке в попередніх записах не заповнено:
Можна заповнити дані для раніше створених записів за допомогою одноразових значень. Для цього в консолі треба прописати значення для заповнення таблиці за дефолтом. В моєму випадку це default title
:
У такому випадку таблиця з попередніми записами буде заповнена. Але також можна використати й багаторазові значення в самій моделі:
Додаємо параметр default
і в ньому описуємо значення для попередніх та нових записів. Після запуску міграції буде створено нове поле — і попередні записи в ньому будуть вже заповнені.
- Залишити поле порожнім
На цьому прикладі я додав поле з датою published_at
, якому дозволено бути пустим. Для цього потрібно вказати два параметри: null=True
та blank=True
.
При створенні запису в таблиці з question
є як обов’язкові поля (наприклад, question_text
), так й інші (в нашому випадку published_at
). Останні завдяки позначці null=True
будуть заповнені зі значенням null
— тобто вони будуть пустими. Дозаповнювати такі поля можна пізніше.
Також є параметр blank=True
. Якщо null
відповідає за поводження у середині бази даних, то blank
— за роботу в адмінці Django. Навіть за наявності поля null=True
при створенні запису адміністративна панель не дозволить зробити пусті записи без поля blank=True
. За допомогою цієї позначки ми можемо створювати в адмінці такі поля.
- Якщо не хочеться ставити default або null
Інколи недоцільно або неможливо дозволити появу пустих полів — бо це не є логічним поводженням моделей. Припустимо, що у нас є дві моделі: Question
та Choice
. Перша — для опису якогось питання, друга — для пулу відповідей на це питання.
В якийсь момент вам потрібно об’єднати чойси з квесченами.
Для цього вказуємо, до якого запитання відноситься кожен question
. Через це ми не можемо дозволити, щоб поле було пустим або заповненим по дефолту. Навіть Django буде казати, щось не так. Адже він розумітиме, як поводитися з попередніми записами без дефолту.
Найпростіший спосіб — знести базу даних, видалити старі міграції, створити нову базу, внести необхідні зміни в моделі та створити міграцію. Вона задасть ці зв’язки, і кожного разу при заповненні бази даних все буде добре. Якщо ж дані в цій базі дуже важливі, то цей спосіб не підходить. Тоді варто створити дублікатну модель для моделі Choice
:
Я назвав її QuestionChoice
. Вона складається з choice_text
та question
із ForeignKey
-зв’язком на Question
. Далі треба зробити міграцію та мігрувати нову модель у базу даних, а потім — створити дата-міграцію. За відомою вже логікою з її допомогою модель QuestionChoice
буде заповнюватись даними з моделі Choice
та зав’язаними на кожен із цих записів ключами на записі в моделі Question
. Після цього можна запускати дата-міграцію та використовувати всі ці зміни. Вам залишиться лише видалити попередню модель Choice
та зробити міграцію з перейменуванням моделі-дубліката QuestionChoice
на звичну Choice
.
Дата-міграції
До цього ми розглядали структурні міграції, які відповідали за зміни структури даних. В описаному сценарії з’явилося поняття дата-міграції. Вона допомагає змігрувати та зберегти дані за нетиповими правилами, які не розуміє Django. Для початку роботи з дата-міграціями треба створити пусту міграцію наступною командою:
manage.py makemigrations --empty yourappname
У результаті ми отримуємо міграцію у такому вигляді:
Наступний крок — заходимо в середину пустої міграції та прописуємо певні дані для дата-міграцій. Насамперед потрібно прописати функцію з логікою, яка має бути виконана:
В моєму випадку виникла необхідність у створенні та заповненні поля name
, яке має включати first_name
та last_name
з попередньої моделі. Тобто ми створюємо функцію і викликаємо її всередині оператора operations
, запускаємо міграції — і далі Django все зробить сам.
Скасування дата-міграції
Іноді потрібно скасувати чи відкотити створену дата-міграцію. Проте якщо використати вже описаний вище механізм, з’явиться наступний warning:
Причина цього — при створенні міграції в автоматичному режимі в неї всередині функції міграції створюється дзеркальна міграція відката.
Фактично з’являються дві міграції: одна — яка мігрує та інша — яка повертає значення назад. При створенні та самостійному описі дата-міграції розробникам не хочеться писати багато коду, тому більшість нехтує створенням зворотної міграції. Через це і виникають складнощі. Тому якщо ви знаєте, що будете часто відкочувати дата-міграцію, краще пропишіть функцію відкату та вкажіть її другим параметром всередині функції RunPython
усередині operations
:
Повторний запуск дата-міграцій
При створенні дата-міграції допустима різна логіка. Наприклад, можна робити її з прицілом на декілька запусків та дозаповнення. Проте інколи це неможливо чи просто не хочеться прописувати функцію відкату. Тоді допоможе так звана фейк-міграція:
Завдяки вказівці fake
можна відкотити дата-міграцію і запустити її знову. Наприклад, ми промігрували до 8-ї міграції, потім відкотились назад до 7-ї за допомогою фейкової міграції:
Після відкочування з оператором fake
повернулися на 7-му міграцію, зробили певні зміни у 8-й міграції та повторно запустили міграцію — і все спрацює, як потрібно:
Фейк-міграції
Хочу окремо зосередити вашу увагу на понятті фейк-міграції.
У міграції є два стани:
- Суттєвий, який можна побачити всередині папки з файлами
migrate
(це ті самі файли міграції). - Це запис в базі даних, де також є таблиця з усіма міграціями, які застосували до бази даних.
Коли створюється фейкова міграція, вона додає або видаляє запис із цієї таблиці, але сама міграція не застосовується в жоден із боків.
Тому при використанні дата-міграції дуже зручно робити фейк-міграції назад або вперед. Так ми додаємо запис у таблицю з міграціями, проте зміни з бази даних не виконуємо.
З фейковими міграціями працюють ті ж самі інструменти, як і зі звичайними. Ми можемо повернутися до початкової міграції, зробити фейк-міграцію застосунку, відкотитися до певної міграції та стерти записи в таблиці, не застосовуючи зміни до бази даних.
Для цього використовуються наступні команди:
manage.py migrate --fake-initial manage.py migrate --fake myapp manage.py migrate --fake myapp migration_name
Але треба пам’ятати: фейкові міграції добре показують себе під час роботи з дата-міграціями. Проте у випадку зі структурними міграціями, які описують структуру застосунку, є певні ризики.
Наприклад, так ви можете видалити запис міграції про створення якоїсь моделі. Тоді в таблиці не буде даних про виконання міграції зі створенням цієї моделі — хоча в базі даних ця модель створена. Тому при повторній міграції цієї таблиці може виникнути помилка, пов’язана зі спробою створення поля, яке вже існує в базі даних.
Squashing migrations
І наостанок розповім про сквош-міграції. Цей інструмент дозволяє зробити структуру міграцій більш лаконічною. Під час тривалої розробки застосунку може накопичуватися 100, 200, 300 та більше файлів з міграціями. Це не проблема для Django, він вміє працювати з такою структурою. Однак для програміста це все незручно. Якщо у вас, скажімо, 200 міграцій, а потрібно відкотитися до сотої, а потім назад, то важко все відслідкувати. Для поєднання та оптимізації міграцій роблять сквош-міграцію.
При використанні сквош-міграції можна в автоматичному режимі оптимізувати міграції всього застосунку або вказати, до якої міграції зробити сквошинг.
У такому випадку Django спочатку проаналізує всі міграції, виконані після вказаної міграції (в моєму прикладі — четвертої). Потім програма зробить їх оптимізацію, подивиться, які дії виконувались і чи суперечать вони одна одній, скомпонує все в один файл — і створить сквош-міграцію:
Після цього необхідно виконати фіксацію сквош-міграції. Тут маємо кілька кроків:
- Вилучити всі міграційні файли, які включає сквош-міграція.
- Оновити всі міграції, які залежали від стиснутих та видалених міграцій.
- Внести зміни в атрибуті
Migration
класі сквош-міграції. Так ми скажемо Django, що це стиснена міграція, і на неї треба орієнтуватися в майбутньому.
Замість висновку
Як бачите, нічого занадто складного у роботі з такими міграціями, як і з будь-якими іншими, в Django немає. Проте не все так легко, як могло здатися спочатку. І про це важливо пам’ятати кожному Python-розробнику.
Цей матеріал – не редакційний, це – особиста думка його автора. Редакція може не поділяти цю думку.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: