ru:https://highload.today/blogs/vse-ne-tak-prosto-kak-kazhetsya-razbiraem-slozhnye-sluchai-migratsii-baz-dannyh-v-django/ ua:https://highload.today/uk/blogs/vse-ne-tak-prosto-yak-zdayetsya-rozbirayemo-skladni-vipadki-migratsiyi-baz-danih-v-django/
logo
Back-end      17/06/2022

Все не так просто, как кажется: разбираем сложные случаи миграции баз данных в Django

Михайло Сердюк BLOG

Backend Developer в NIX

При знакомстве фреймворком Django многие одобрительно оценивают механизм миграций. С его помощью можно синхронизировать код в моделях Django с базой данных, подключенной к веб-приложению. При этом миграции происходят автоматически, что очень облегчает работу. Но так ли все хорошо, как выглядит на первый взгляд? По собственному опыту скажу, что на практике с миграциями может быть много проблем.

Меня зовут Михаил Сердюк, я Backend Developer в NIX и спикер IT-конференции NIX MultiConf. В этой статье я расскажу, как решить проблемы, которые могут возникнуть во время миграций в Django. Погнали!

Содержание

1. Что такое миграции
2. Зачем разбираться с миграциями
3. Как заменить миграцию
4. Сложные случаи при миграциях
5. Дата-миграции
6. Отмена дата-миграции
7. Повторный запуск дата-миграции
8. Фейк-миграции
9. Squash migration
10. Вместо вывода

Что такое миграции

Для начала — немного теории. Предположим, что в Django-приложении в файле моделей мы указали определенный класс, чтобы описать, как должна выглядеть наша таблица с данными:

 

В таком случае с созданием миграций все было бы хорошо. Но представим, что в какой-то момент возникла необходимость дополнить модель.

Например, в Question нужно добавить поле для описания title вопроса. Может показаться, что достаточно просто вписать его, но на самом деле этого мало. Чтобы Django воспринял эти изменения и занес их в базу данных, модель нужно промигрировать в Django. В следующем примере я намеренно задал для поля title параметр blank=True. Чуть дальше по тексту объясню это:

 

Курс QA.
Найпростіший шлях розпочати кар'єру в ІТ та ще й з гарантованим працевлаштуванням.
Приєднатися

Итак, мы изменили базу данных. Теперь нужно создать миграцию. Для этого прописываем команду manage.py makemigrations. После этого Django автоматически создаст файл с миграциями. Эти файлы создаются в хронологическом порядке, по нумерации. Когда мы первый раз запускаем приложение, появляется initial-миграция, а после нее — следующие. В нашем случае это вторая миграция с прописанными в модели переменами.

Но у нас появились только файлы, сравнившие состояние файла с моделями приложения. Для этого Django проанализировал файлы миграции, созданные ранее, с классами и моделями в файле model.py. Разница между ними и стала основой файла миграции. Но для использования этого файла в базе данных необходимо запустить еще одну команду —manage.py migrate.

После этого в консоли появится вывод — миграции применены. При первом запуске приложения этот вывод очень массивный, ведь у Django есть много встроенных моделей, которые применяются в первую очередь при миграции:

 

Давайте разоберемся, что представляет собой сам файл с миграцией:

  • Во-первых, в нем указываются зависимости, из которых он создан. Для этого делается ссылка из базы данных на последнюю примененную миграцию.
  • Во-вторых, в файле указываются требуемые изменения в базе данных. В нашем случае это создание в модели question нового поля с названием title и параметры для него:

Зачем разбираться с миграциями

Может показаться, что здесь достаточно оперировать только двумя командами: makemigrations и migration. На небольших проектах именно так и происходит: пишете и применяете миграции, а также добавляете код для оперирования данными. Но часто заказчик формирует задачу очень отвлеченно.

Объясню по аналогии. Клиенту требуется транспорт, на котором он сможет добраться из пункта А до пункта Б и перевезти груз. Исполнитель, как рациональный человек, пойдет по пути наименьшего сопротивления и сделает велосипед:

  • Это транспорт? Да.
  • На нем можно добраться из пункта А до пункта Б? Можно.
  • Он подходит для перевозки груза? Безусловно.

И если вернуться к нашим с вами задачам, то подобная ситуация может привести к схеме базы данных, приведенной на этой иллюстрации:

Это пример первой реализации базы данных, взятый из практики. Здесь были обширные модели, у них были свойства, связанные с основными моделями. Но заказчик сказал, что у него есть несколько замечаний.

Если вернуться к нашей аналогии, ситуация выглядела примерно так:

  • во-первых, расстояние от пункта А до пункта Б большое, поэтому требуется транспорт намного быстрее велосипеда;
  • во-вторых, по пути немало подъемов и спусков, поэтому вместо педалей лучше установить двигатель;
  • Курс Англійської.
    Тут навчають за методикою Кембриджу, завдяки якій англійську вивчили понад 1 мільярд людей. Саме вона використовується в найкращих навчальних закладах світу, і саме за нею створені курси.
    Реєстрація на курс
  • в-третьих, использование транспорта планируется в течение года, поэтому необходима кабина с обогревателем и кондиционером;
  • да и вообще грузы большие, и багажника на велосипеде маловато.

После замечаний разработчик вносит изменения в структуру базы данных и впоследствии проект трансформируется. Результаты изменений можно увидеть на иллюстрации ниже. Кстати, эта схема еще не конечный вариант, а где-то 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-й миграции и повторно запустили миграцию — и все сработает, как нужно:

Фейк-миграции

Хочу отдельно сосредоточить ваше внимание на понятии фейк-миграции.

У миграции есть два состояния:

  1. Существенная, которую можно увидеть внутри папки с файлами migrate (это те же файлы миграции).
  2. Запись в базе данных, где также содержится таблица со всеми миграциями, которые применили к БД.

Когда создается фейковая миграция, она добавляет или удаляет запись из этой таблицы, но сама миграция не применяется ни в одну сторону.

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

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

Для этого используются следующие команды:

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-разработчику.

If you have found a spelling error, please, notify us by selecting that text and pressing Ctrl+Enter.

SQL для аналітики.
Навчіться аналізувати дані за допомогою власного SQL коду.
Зареєструватися

Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.

Топ-5 самых популярных блогеров февраля

Всего просмотровВсего просмотров
229
#1
Всего просмотровВсего просмотров
229
Всего просмотровВсего просмотров
209
#2
Всего просмотровВсего просмотров
209
QA в CodeGeeks Solutions
Всего просмотровВсего просмотров
156
#3
Всего просмотровВсего просмотров
156
Senior Project Manager at Nemesis
Всего просмотровВсего просмотров
99
#4
Всего просмотровВсего просмотров
99
Software Architect at Devlify
Всего просмотровВсего просмотров
95
#5
Всего просмотровВсего просмотров
95
Рейтинг блогеров

Ваша жалоба отправлена модератору

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: