Удаление ненулевого поля Django с помощью SeparateDatabaseAndState

Допустим, у меня есть следующая модель:

class Product(models.Model):
    name = models.CharField(max_length=128)
    is_retired = models.BooleanField(default=False)

Я хочу удалить поле is_retired. Я использую сине-зеленые развертывания для выпуска изменений в производство, поэтому я использую SeparateDatabaseAndState в моей миграции.

Моя первоначальная миграция для удаления поля из состояния приложения проста:

class Migration(migrations.Migration):
    dependencies = ...
    operations = [
        migrations.SeparateDatabaseAndState(
            state_operations=[
                migrations.RemoveField(
                    model_name="product",
                    name="is_retired",
                ),
            ],
            database_operations=[],
        ),
    ]

Я могу успешно запустить миграцию. Однако при попытке создать новый продукт с помощью Product.objects.create(name="Wrench"):

я получаю следующую ошибку
self = <django.db.backends.sqlite3.base.SQLiteCursorWrapper object at 0x106bf4050>
query = 'INSERT INTO "core_product" ("name") VALUES (?) RETURNING "core_product"."id"'
params = ('Wrench',)

    def execute(self, query, params=None):
        if params is None:
            return super().execute(query)
        # Extract names if params is a mapping, i.e. "pyformat" style is used.
        param_names = list(params) if isinstance(params, Mapping) else None
        query = self.convert_query(query, param_names=param_names)
>       return super().execute(query, params)
E       django.db.utils.IntegrityError: NOT NULL constraint failed: core_product.is_retired

Похоже, что запрос INSERT терпит неудачу, потому что поле is_retired по умолчанию принимает значение null, а не правильное значение по умолчанию False.

Я исправил это, сделав Product.is_retired обнуляемым перед удалением из состояния:

class Migration(migrations.Migration):
    dependencies = ...
    operations = [
        migrations.AlterField(
            model_name="product",
            name="is_retired",
            field=models.BooleanField(default=False, null=True),
        ),
        migrations.SeparateDatabaseAndState(
            state_operations=[
                migrations.RemoveField(
                    model_name="product",
                    name="is_retired",
                ),
            ],
            database_operations=[],
        ),
    ]

Теперь я могу успешно создать продукт с помощью Product.objects.create(name="Wrench").

Несколько вопросов:

  • Является ли это лучшим способом исправить эту ошибку? Есть другие идеи? Я пробовал изменить значение по умолчанию is_retired обратно на False, но так и не смог разобраться.
  • Безопасно ли изменение поля с not nullable на nullable во время развертывания "сине-зеленого"? Я почти уверен, что да, но просто хочу убедиться.

Ваш подход к тому, чтобы сделать поле is_retired обнуляемым перед удалением его из состояния, является правильным решением проблемы, с которой вы столкнулись. Однако давайте обратимся непосредственно к вашим вопросам:

Является ли это лучшим способом исправить эту ошибку? Есть другие идеи?

Ваш подход является разумным. Сделав поле нулевым перед удалением его из состояния, вы избежите сбоя ограничения NOT NULL при вставке новых записей. Другой подход может включать ручную установку значения поля по умолчанию в базе данных перед запуском миграции для удаления его из состояния, но это может потребовать большего количества ручных операций с базой данных и может привести к ошибкам.

Безопасно ли изменение поля с not nullable на nullable во время развертывания сине-зеленого цвета?

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

Ваш подход по сути правильный и подойдет для большинства сценариев. Насколько я знаю, он также не опасен. Однако в зависимости от важности поля модели в вашем приложении и стратегии выпуска функций в синем/зеленом развертывании, вы можете захотеть усилить контроль над полем модели is_retired.

Идея флага функции в случае синего/зеленого развертывания интересна тем, что она позволяет задействовать или не задействовать определенную функциональность в определенный момент времени. В случае is_retired вы можете захотеть, чтобы она была активна для некоторых пользователей, постепенно отказываясь от нее во всем приложении (приложениях).

Вот базовая реализация:

в вашем settings.py

FEATURE_FLAGS = {
    'IS_RETIRED_ENABLED': os.getenv('IS_RETIRED_ENABLED', 'False') == 'True',
}

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

тогда в вашем models.py

class Product(models.Model):
    name = models.CharField(max_length=128)
    is_retired = models.BooleanField(default=False, null=True)

    @staticmethod
    def create_product(name, is_retired=None):
        if settings.FEATURE_FLAGS['IS_RETIRED_ENABLED']:
            return Product.objects.create(name=name, is_retired=is_retired)
        else:
            return Product.objects.create(name=name)

Технически вы также можете разместить эту функцию в файле views.py или где угодно, но в большинстве случаев лучше держать логику близко к модели.

Наконец, в различных средах, для которых вы хотите использовать или не использовать поле is_retired, вы можете сделать :

export IS_RETIRED_ENABLED=True

or 

export IS_RETIRED_ENABLED=False

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

Вернуться на верх