Django doesn't throw a ValidationError
because of a missing conditional in UniqueConstraint
, and I don't know how to formulate a correct one.
One of my models contains a unique constraint including a Foreign Key:
class Entry(models.Model):
"""
Entry on a List
"""
name= models.CharField()
expiration_date = models.DateField()
list = models.ForeignKey(List,
related_name="entries",
on_delete=models.CASCADE)
...
class Meta:
constraints = [
UniqueConstraint(fields=['list', 'name'],
name='unique_entry') # Each entry_name may only occur once per list.
]
When submitting a new Entry which violates this constraint, the database rejects the query and Django throws an unhandled exception IntegrityError
.
According to the Django documentation this is intended behaviour:
Validation of Constraints
In general constraints are not checked during full_clean(), and do not raise ValidationErrors. Rather you’ll get a database integrity error on save(). UniqueConstraints without a condition (i.e. non-partial unique constraints) are different in this regard, in that they leverage the existing validate_unique() logic, and thus enable two-stage validation. In addition to IntegrityError on save(), ValidationError is also raised during model validation when the UniqueConstraint is violated.
I would like your help with formulating a conditional, or other suggested solutions to fix this behaviour. The goal is to treat the UniqueConstraint like any other field that won't validate: Django throws a ValidationError which is caught by Django Rest Framework, and ultimately the original request will receive a HTTP 400 Bad Request, instead of 500 Internal Server Error.
Ideally I would like to implement a solution which facilitates throwing a ValidationError
instead of IntegrityError
from the model, instead of having to resort to the Serializer
or View
(The alternative solution I would like to avoid is querying the database from the View
to validate the unique constraint.).
An example would be condition=Q(name=self.name, list=self.list)
.
However, I can't refer to the model instance from within Meta
:
class Meta:
constraints = [
UniqueConstraint(fields=['list', 'name'],
condition=Q(name=self.name, list=self.list), # this is incorrect
name='unique_entry')
]
Am I trying to do something impossible? Thank you in advance, ideas and suggestions would be much appreciated.
You can add a check in the clean
method of your model. If you then use a ModelForm
, it will call the clean()
method, and thus check if the item is valid.
The model thus then looks like:
from django.core.exceptions import ValidationError
class Entry(models.Model):
name= models.CharField()
expiration_date = models.DateField()
list = models.ForeignKey(
List,
related_name='entries',
on_delete=models.CASCADE
)
def clean(self, *args, **kwargs):
qs = Entry.objects.exclude(pk=self.pk).filter(name=self.name, list_id=self.list_id)
if qs.exists():
raise ValidationError('Can not add an entry with the same name to a list')
return super().clean(*args, **kwargs)
class Meta:
constraints = [
UniqueConstraint(
fields=['list', 'name'],
name='unique_entry'
)
]