Django молча удаляет ограничения при удалении столбцов, а затем произвольно решает включить их в миграции
Я столкнулся со странной ошибкой(?) в Django в нашем производственном коде. Уменьшение колонки вручную в миграциях без удаления ее ограничений приводит к тому, что Django не знает, что ограничения были удалены, и автоматически генерирует неправильные миграции.
Короткий пример:
Создание миграций для этой схемы:
class M():
a = models.IntegerField(default=1)
b = models.IntegerField(default=2)
class Meta:
constraints = [
UniqueConstraint(
name="uniq_ab_1”, fields=[“a”, “b”]
)
]
Создание ограничения uniq_ab_1 происходит в Postgres, как и ожидалось (при использовании команды \d+ на таблице для визуализации)
Однако, эта ручная миграция удалит ограничение из-за удаления одного из его столбцов-членов, это просто стандартное поведение Postgres:
migrations.RemoveField(
model_name=“m”,
name=“a”,
),
migrations.AddField(
model_name=“m”,
name=“a”,
field=models.IntegerField(default=6),
),
Эта миграция выполняется просто отлично. Я даже могу снова изменить поле M и запустить дальнейшую миграцию. Однако при использовании \d+ обнаруживается, что ограничение uniq_ab_1 исчезло из базы данных.
Единственный способ, которым я узнал об этом поведении, - переименовать ограничение в uniq_ab_2, затем автоматически сгенерировать миграции и получить ошибку:
django.db.utils.ProgrammingError: constraint "uniq_ab_1" of relation … does not exist. Другими словами, при переименовании Django понял, что происходит переименование, и попытался удалить ограничение из базы данных, несмотря на то, что его не было в течение нескольких миграций.
Такое поведение довольно неожиданно. Я бы предположил, что Django либо:
- Notice the code model differs from the database schema in the original migration (when the constraint gets inadvertently remove) and fail
- Notice that the constraint is missing in the next migration (e.g. when adding an arbitrary field to
M) and try to add it back again.
Как видно, это призрачное ограничение, которое соблюдается одними операциями миграции и не соблюдается другими.
Является ли это известной ошибкой в Django? Есть ли способ защититься от такого поведения? Это совершенно нормально и я делаю что-то не так?
В вопросе утверждается несколько неверных представлений о Django.
Django молча удаляет ограничения при удалении столбцов
Нет, Django этого не делает. Ограничение автоматически снимается PostgreSQL.
.
https://postgrespro.com/docs/postgresql/9.6/sql-altertable
Вы можете проверить, что делает Django с помощью python manage.py sqlmigrate.
[Django] произвольно выбирает включение [ограничений] в миграции
Нет, Django не делает этого. Django всегда учитывает ограничения при генерации миграций с помощью python manage.py makemigrations.
Другими словами, при переименовании Django узнал о переименовании и попытался удалить ограничение из базы данных, несмотря на то, что оно отсутствовало в течение нескольких миграций.
Django не знает, что происходит переименование. Django просто удаляет ранее существующую и добавляет новую, сравнивая определение модели и схему модели, построенную на основе существующих миграций.
Такое поведение довольно неожиданно. Я бы предположил, что Django либо:
- Заметит, что модель кода отличается от схемы базы данных в оригинальной миграции (когда ограничение случайно удалено) и потерпит неудачу
.- Заметить, что ограничение отсутствует в следующей миграции (например, при добавлении произвольного поля в M) и попытаться добавить его снова.
Django проверяет, что определение модели не имеет проблем в начале python manage.py migrate, но не сравнивает его со схемой базы данных на каждом шаге operations в миграции.
Когда вы создаете ручную миграцию, вышеуказанное является вашей обязанностью.
Является ли это известной ошибкой в Django? Является ли это совершенно нормальным и я делаю что-то не так?
Как объяснялось выше, это не ошибка и вызвано неправильно спланированной ручной миграцией.
Хотя Django может помочь защититься от этой неправильной ручной операции (показано ниже), существует слишком много способов, которыми пользователь может испортить ручную миграцию, чтобы Django мог их разумно охватить.
Решение
Есть ли способ защититься от такого поведения?
Для блокировки удаления полей, имеющих уникальные ограничения в определении модели, можно использовать патч
BaseDatabaseSchemaEditor.remove_field:def _patch_remove_field(): from itertools import chain from django.core import checks from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.models.constraints import UniqueConstraint old_remove_field = BaseDatabaseSchemaEditor.remove_field def remove_field(self, model, field): field_names = set(chain.from_iterable( (*constraint.fields, *constraint.include) for constraint in model._meta.constraints if isinstance(constraint, UniqueConstraint) )) if field.name in field_names: raise ValueError(checks.Error( "Cannot remove field '%s' as '%s' refers to it" % (field.name, 'constraints'), obj=model, id='models.E012', )) old_remove_field(self, model, field) BaseDatabaseSchemaEditor.remove_field = remove_field