Удаление ненулевого поля 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
перед процессом миграции, и может не подойти для некоторых случаев, особенно для не очень важных полей, но полезно знать, что такая возможность доступна, если вам нужна большая гибкость.