Ограничение на поле модели "вместе ноль или вообще нет"

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

Например, в следующей модели:

from django.db import models

class Example(models.Model):
    A = models.CharField(max_length=16, blank=True)
    B = models.DateField(null=True, blank=True)
    C = models.FileField(upload_to='/', null=True)

Если я пытаюсь создать новый Example с пустыми значениями B или C, то это должно вызвать ошибку ValidationError; но если оба значения пусты, то все должно быть в порядке.

Вы можете использовать CheckConstraint:

from django.db.models import CheckConstraint, Q

class Example(models.Model):
    ...
    class Meta:
        constraints = [
            CheckConstraint(
                check=(
                    (Q(B__isnull=True) & Q(C__isnull=True))
                    | Q(B__isnull=False) | Q(C__isnull=False)
                ),
                name="b_c_null_check",
            )
        ]

Ответ @Selcukis правильный, но я хотел получить более общий ответ. Взяв его ответ за основу, я сделал следующий класс, который наследует от CheckConstraint, но вы можете передать аргумент fields вместо аргумента check для создания ограничения:

from django.db.models import CheckConstraint, Q
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _

class EmptyTogether(CheckConstraint):
    def __init__(self, *, fields=None, check=None, name, violation_error_message=None):
        if fields is not None and check is not None:
            raise ImproperlyConfigured(
                _("Creating an EmptyTogether constraint with 'fields' and 'check' attributes is prohibited")
            )
        if check is not None:
            super().__init__(check=check,name=name,violation_error_message=violation_error_message)
            return
        if fields is None:
            raise ImproperlyConfigured(
                _("Creating an EmptyTogether constraint without the 'fields' attribute "
                f"is prohibited; constraint {name} needs updating.")
            )
        
        if isinstance(fields,str) or not hasattr(fields, "__iter__"):
            raise ImproperlyConfigured(
                _("EmptyTogether constraint 'fields' attribute should be an iterable")
            )
        
        allNull = ~Q()
        someNull = Q()
        for field in fields:
            q = Q(**{f'{field}__isnull':True})
            if not isinstance(field,str):
                if not (hasattr(field, "__iter__") and len(field)==2):
                    raise ImproperlyConfigured(
                        _("EmptyTogether constraint 'fields' should only contain non empty string values"
                        "or tuples (str,value)")
                    )
                key,value = field[0],field[1]
                if not isinstance(key,str):
                    raise ImproperlyConfigured(
                        _("EmptyTogether constraint 'fields' should only contain non empty string values"
                        "or tuples (str,value)")
                    )
                q = Q(**{key:value})
            allNull &= q
            someNull |= q
        check = (allNull) | ~(someNull)
        
        super().__init__(check=check,name=name,violation_error_message=violation_error_message)

Я размещаю его здесь, чтобы если кто-то столкнется с этой проблемой, и ему понадобится проверить empty_together по слишком большому количеству полей во многих моделях, и ему нужен адаптируемый способ сделать это. Просто создайте utils.py в вашем проекте django и импортируйте его при необходимости

Применительно к расширенному примеру это будет что-то вроде:

from django.db import models
from PROYECT.utils import EmptyTogether

class Example(models.Model):
    A = models.CharField(max_length=16, blank=True)
    B = models.DateField(null=True, blank=True)
    C = models.FileField(upload_to='/', null=True)
    D = models.DecimalField(max_digits=6, decimal_places=2, default=0)

    class Meta:
        constraints = [
            EmptyTogether(
                fields=( ('A',''), 'B', ('D',0) ),
                name="a_b_d_empty_check",
            )
        ]

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

from django.db.models import CheckConstraint, Q

class EmptyTogether(CheckConstraint):
    def __init__(self, *, fields=None, check=None, name, violation_error_message=None):
        if check is not None:
            super().__init__(check=check,name=name,violation_error_message=violation_error_message)
            return

        allNull = ~Q()
        someNull = Q()
        for field in fields:
            q = Q(**{f'{field}__isnull':True})
            if not isinstance(field,str):
                key,value = field
                q = Q(**{key:value})
            allNull &= q
            someNull |= q
        check = (allNull) | ~(someNull)
        super().__init__(check=check,name=name,violation_error_message=violation_error_message)
Вернуться на верх