Удаление столбца из модели Django в рабочей среде
24 января 2023 г.Как изменить код Django во время простого развертывания на рабочем сервере
В зависимости от среды работа с Django не всегда так проста, как показано в руководствах. Это может показаться простым — если вам нужно добавить столбец, добавьте поле в модель, создайте миграцию, запустите ее, и столбец готов. А, если надо удалить, то все так же просто: удаляем поле, создаем миграцию, запускаем и готово. Однако в производственной среде это не всегда работает.
Когда у вас есть несколько серверов или контейнеров, работающих на Prod, и миграции публикуются до публикации кода, становится важным не нарушить бесшовное развертывание. То есть нужно поддерживать непротиворечивость базы данных и кода и даже больше.
В предыдущей статье я писал о том, , как развернуть изменения модели Django в рабочей среде. а>. Перейдем к деталям.
Удаление столбца в производстве
В зависимости от архитектуры развертывания для удаления столбца в рабочей среде можно использовать несколько методов.
- Выполнение миграции после развертывания кода во всех модулях в рамках одного развертывания.
- Выполнение миграции со вторым развертыванием сразу после первого, в котором был развернут код.
- Выполнение миграции с отдельным развертыванием после развертывания кода, но с возможной задержкой и даже другими развертываниями.
А теперь давайте рассмотрим это более подробно. В качестве примера возьмем модель списка аэропортов.
class Airport(models.Model):
iata = models.CharField(max_length=3, unique=True)
name = models.CharField(max_length=30, unique=True)
Если вы удалите столбец «имя» из модели и не будете создавать или выполнять миграции, ничего страшного не произойдет. Но если вы попытаетесь начать создавать миграции, миграция будет добавлена с удалением этого поля:
python manage.py makemigrations
Тем не менее, держать в базе данных лишнее неиспользуемое поле — не лучшая идея. Поэтому кажется логичным создать миграцию, а затем выполнить ее, тем самым удалив это поле из таблицы в базе данных.
В итоге проверка на локальной машине показывает, что все работает. Но когда мы начали деплоить в Prod, появляется куча ошибок. Итак, на Prod все по-другому. При развертывании модулей с новым кодом чаще всего происходит следующее.
- Инициализация выполняется перед каждым развертыванием, включая выполнение миграции:
python manage.py migrate
- После этого создаются пакеты по одному. После создания модуля один из старых модулей уничтожается.
Теперь представьте, что миграции завершены, но старый код все еще работает. И он будет работать какое-то время в зависимости от количества и сложности модулей, иногда в течение нескольких секунд, а иногда в течение нескольких минут или даже десятков минут.
Итак, после завершения миграции, до замены последнего пода, почти любой запрос к модели Airport будет генерировать исключение. Например:
Airport.objects.first()
вызовет исключение, например:
ProgrammingError: column avia_airport.name does not exist
LINE 1: SELECT "avia_airport"."id", "avia_airport"."iata", "avia_airport"."name"
Решения
Первое решение предполагает наличие двух типов переноса: один выполняется до развертывания кода, а другой — после него. Если у вас это в CI/CD, то все просто. Добавление столбца, создание модели и т. д. необходимо выполнить до развертывания, а удаление — после этого.
Второе решение возможно только в том случае, если вы полностью уверены, что сможете выполнить два развертывания подряд без перерыва и за это время никто ничего не развернет и не изменит. Затем вы создаете миграцию, как обычно. После этого вы сначала развертываете только код, а затем запускаете миграцию.
Класс модели для двух предыдущих методов будет выглядеть следующим образом:
class Airport(models.Model):
iata = models.CharField(max_length=3, unique=True)
И миграция будет выглядеть следующим образом:
migrations.RemoveField(
model_name='avia_airport',
name='name',
)
И, наконец, третье решение для тех случаев, когда вы не можете гарантировать, что две версии будут развернуты подряд без каких-либо помех. В этом случае вам нужно создать «поддельную» миграцию. Для этого используется state_operations. Например:
migrations.RunSQL(
sql=migrations.RunSQL.noop,
reverse_sql=migrations.RunSQL.noop,
state_operations=[
migrations.RemoveField(
model_name='avia_airport',
name='name',
),
]
)
Таким образом вы сообщаете Django, что эта миграция завершена. И когда вы запускаете команду makemigrations
, Django не добавит миграцию с удалением поля «имя». Однако эта миграция не удалит это поле из таблицы в базе данных. Итак, ваше первое развертывание будет содержать удаление поля из модели в коде и «фальшивую» миграцию.
Затем второе развертывание должно будет содержать код удаления столбца. Но поскольку Django считает, что столбец был удален при предыдущей миграции, его придется удалить с помощью команд SQL.
migrations.RunSQL(
sql="ALTER TABLE public.avia_airport DROP COLUMN name CASCADE",
reverse_sql=migrations.RunSQL.noop
)
Если вы не уверены, что писать в SQL, создайте оригинальную миграцию и выполните команду sqlmigrate
:
python manage.py sqlmigrate avia
Вы увидите нужную команду SQL, которую вы можете скопировать.
Кстати, я настоятельно рекомендую указывать reverse_sql
при каждой ручной миграции. Если у вас есть специальный сценарий отката миграции, его следует запустить reverse_sql
. Если у вас его нет, используйте migrations.RunSQL.noop
для миграции RunSQL
или migrations.RunPython.noop
для RunPython
миграции — пустая миграция. Это поможет избежать исключения в случае необходимости отката миграции.
Заключение
Всегда помните, что в рабочей среде запущено несколько экземпляров Django, которые последовательно обновляются до новой версии. Также помните, что между выполнением миграции и обновлением кода есть период времени, в течение которого пользователи могут увидеть ошибку. Это может нанести репутационный и финансовый ущерб. Поэтому используйте многоэтапное развертывание при изменении базы данных.
Оригинал