2025, Sep 23 13:16

Ограничения Django не работают при managed=False: почему и что делать

Почему при managed=False в Django не срабатывают CheckConstraint и UniqueConstraint, не возникает IntegrityError и как вынести ограничения на уровень БД.

Когда ограничения базы данных в модели Django упорно не вызывают IntegrityError в тестах, хочется списать это на выражения Q или неверную конфигурацию теста. Но если у модели задано managed = False, Django вообще не трогает схему таблицы. В итоге ограничения, объявленные в модели, просто не попадают в базу, и проверять там нечего. Результат: при save() не возникает никаких исключений, как бы ни были некорректны данные.

Постановка задачи

Рассмотрим модель, в которой объявлено несколько ограничений, в том числе CheckConstraint для ограничения статусов и правило, запрещающее устанавливать date_published, когда продукт в состояниях Draft или Discontinued. В модели также есть несколько UniqueConstraint. В тестах пытаются нарушить эти правила, чтобы получить IntegrityError.

from django.db import models
from django.db.models import CheckConstraint, Q, UniqueConstraint


class CatalogItem(models.Model):
    code = models.CharField(primary_key=True, max_length=8)
    ean = models.CharField(unique=True, max_length=14, blank=True, null=True)
    title = models.TextField(blank=True, null=True)
    msrp = models.DecimalField(max_digits=8, decimal_places=2, blank=True, null=True)
    state = models.TextField()
    maker = models.TextField(blank=True, null=True)
    label = models.TextField(blank=True, null=True)
    origin = models.TextField(blank=True, null=True)
    updated_at = models.DateField(blank=True, null=True)
    published_at = models.DateField(blank=True, null=True)
    is_exclusive = models.BooleanField(blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'product'
        constraints = [
            CheckConstraint(
                condition=Q(state__in=['Draft', 'Live', 'Discontinued']),
                name='ck_state_allowed',
                violation_error_message="status field must be on of the following: 'Draft', 'Live', 'Discontinued'",
            ),
            CheckConstraint(
                condition=~(Q(published_at__isnull=False) & Q(state__in=['Draft', 'Discontinued'])),
                name='ck_published_vs_state',
                violation_error_message="Product with status 'Draft' or 'Discontinued' cannot have a date_published value",
            ),
            UniqueConstraint(fields=['ean'], name='uq_ean'),
            UniqueConstraint(fields=['code', 'is_exclusive'], name='uq_code_exclusive'),
            UniqueConstraint(fields=['code', 'state'], name='uq_code_state'),
        ]

Тест пытается сохранить недопустимое значение state и ожидает IntegrityError:

from django.db.utils import IntegrityError
from django.test import TestCase

from .models import CatalogItem


class CatalogItemTests(TestCase):
    def setUp(self):
        self.item = CatalogItem.objects.create(
            code='12345678',
            ean='5666777888999',
            title='Test Product 1',
            msrp=999,
            state='Live',
            maker='',
            label='Test Brand',
            origin='China',
            updated_at='2025-01-01',
            published_at='2025-01-01',
            is_exclusive=False,
        )

    def test_invalid_state_rejected(self):
        self.item.state = 'WrongStatus'
        with self.assertRaises(IntegrityError):
            self.item.save()

Что происходит на самом деле

В Django ограничения из Meta.constraints применяет именно база данных. Django переводит CheckConstraint в проверку на уровне БД, например CHECK status IN ('Draft', 'Live', 'Discontinued'). Решение о вставке или обновлении строки принимает база; если правило нарушено, она поднимает ошибку целостности, которую Django отображает как django.db.utils.IntegrityError.

Но managed = False полностью меняет картину. При отключённом управлении Django не создаёт, не изменяет и не удаляет таблицу и её ограничения. Он не добавляет поля, не ставит CHECK и UNIQUE. В результате ограничения, перечисленные в модели, отсутствуют в базе и, соответственно, не могут сработать.

Решение

Если модель неуправляемая, объявленные в Django ограничения не будут применены к схеме БД. Чтобы они реально заработали, добавьте эквивалентные ограничения непосредственно в базу. Например, ограничение статуса превращается в нативный CHECK на уровне БД, что в SQL выглядит так:

CHECK (status IN ('Draft', 'Live', 'Discontinued'))

То же относится и к остальным правилам: закрепите их непосредственно в базе данных, чтобы PostgreSQL 14.18 отклонял некорректные строки и поднимал ошибку целостности.

Когда «контракт» принадлежит базе, save() пробрасывает нарушения как IntegrityError, и тесты, которые рассчитывают на эти ошибки, могут уверенно полагаться на проверку со стороны БД.

Почему это важно

Жёсткость ограничений определяется местом их применения. В Django Meta.constraints фиксирует намерение и, когда таблицы управляемые, приводит к миграциям, устанавливающим эти правила в базе. Когда управление отключено, эта связка рвётся. Код по‑прежнему объявляет ограничения, но база о них не знает, поэтому при записи ничего не падает. Понимание этой границы снимает ложные ожидания и в тестах, и в продакшене.

Выводы

Ограничения, объявленные в Django, применяются базой данных, а не Python‑кодом в save(). При managed = False Django не синхронизирует эти правила с таблицей, поэтому они не срабатывают ни в тестах, ни в продакшене. Чтобы полагаться на IntegrityError от этих правил, добавьте соответствующие ограничения напрямую в базу — именно там происходит их реальное применение.

Статья основана на вопросе на StackOverflow от dabo_tusev и ответе willeM_ Van Onsem.