Search code examples
djangodjango-modelsdjango-rest-frameworkunique-constraint

Formulate conditional Q on Unique Constraint for exception handling


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.


Solution

  • 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'
                )
            ]