Удаление столбца из модели Django в рабочей среде

Удаление столбца из модели Django в рабочей среде

24 января 2023 г.

Как изменить код Django во время простого развертывания на рабочем сервере

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

Когда у вас есть несколько серверов или контейнеров, работающих на Prod, и миграции публикуются до публикации кода, становится важным не нарушить бесшовное развертывание. То есть нужно поддерживать непротиворечивость базы данных и кода и даже больше.

В предыдущей статье я писал о том, , как развернуть изменения модели Django в рабочей среде. . Перейдем к деталям.

Удаление столбца в производстве

В зависимости от архитектуры развертывания для удаления столбца в рабочей среде можно использовать несколько методов.

  1. Выполнение миграции после развертывания кода во всех модулях в рамках одного развертывания.
  2. Выполнение миграции со вторым развертыванием сразу после первого, в котором был развернут код.
  3. Выполнение миграции с отдельным развертыванием после развертывания кода, но с возможной задержкой и даже другими развертываниями.

А теперь давайте рассмотрим это более подробно. В качестве примера возьмем модель списка аэропортов.

Если вы удалите столбец «имя» из модели и не будете создавать или выполнять миграции, ничего страшного не произойдет. Но если вы попытаетесь начать создавать миграции, миграция будет добавлена ​​с удалением этого поля:

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

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

  1. Инициализация выполняется перед каждым развертыванием, включая выполнение миграции: python manage.py migrate
  2. После этого создаются пакеты по одному. После создания модуля один из старых модулей уничтожается.

Migrate - create - destroy steps

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

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

вызовет исключение, например:

Решения

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


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE