Hello there,
Partial unique constraints are currently not supported during validation
for reasons described in this ticket[0].
For example (inspired by this Github comment[1]), if you define the
following model
class Article(models.Model):
slug = models.CharField(max_length=100)
deleted_at = models.DateTimeField(null=True)
class Meta:
constraints = [
UniqueConstraint('slug', condition=Q(deleted_at=None),
name='unique_slug'),
]
Then validate_unique must perform the following query to determine if the
constraint is violated
SELECT NOT (%(deleted_at)s IS NULL) OR NOT EXISTS(SELECT 1 FROM article
WHERE NOT id = %(id)s AND slug = %(slug)s AND deleted_at IS NULL)
In other words, the validation of a partial unique constraint must check
that either of these conditions are true
1. The provided instance doesn't match the condition
2. There's no existing rows matching the unique constraint (excluding the
current instance if it already exists)
This is not something Django supports right now.
In order to add proper support for this feature I believe (personal opinion
here feedback is welcome) we should follow these steps:
1. Add support for Expression.check(using: str) -> bool that would
translate IsNull(deleted_at, True).check('alias') into a backend compatible
'SELECT %(deleted_at)s IS NULL' query and return whether or not it passed.
That would also allow the constructions of forms like
(~Q(IsNull(deleted_at, True)) |
~Exists(Article.objects.exclude(pk=pk).filter(slug=slug,
deleted_at=None)).check(using)
2. Add support for Constraint.validate(instance, excluded_fields) as
described in [0] that would build on top of Expression.check to implement
proper UniqueConstraint, CheckConstraint, and ExclusionConstraint
validation and allow for third-party app (e.g. django-rest-framework which
doesn't use model level validation[2]) to take advantage of this feature.
For example the unique_for_(date|month|year) feature of Date(Time)?Field
could be deprecated in favour of Constraint subclasses that implement
as_sql to enforce SQL level constraint if available by the current backend
and implement .validate to replace the special case logic we have currently
in place for these options[3].
I hope this clarify the current situation.
Cheers,
Simon
[0] https://code.djangoproject.com/ticket/30581#comment:7
[1] https://github.com/django/django/pull/10796#discussion_r244216763
[2] https://github.com/encode/django-rest-framework/issues/7173
[3]
https://github.com/django/django/blob/e703b152c6148ddda1b072a4353e9a41dca87f90/django/db/models/base.py#L1062-L1084
Le mardi 1 juin 2021 à 11:18:23 UTC-4, [email protected] a écrit :
> Hi,
>
> I changed several models from fields using `unique=True` to using
> `UniqueConstraint` with a condition in the Meta.
>
> As a side-effect, the uniqueness are no longer validated during cleaning
> of a Form and an integrity error is raised. This is because partial unique
> indexes are excluded :
>
> https://github.com/django/django/blob/e703b152c6148ddda1b072a4353e9a41dca87f90/django/db/models/options.py#L865-L874
>
> It seems that `total_unique_constraints` is also used to check for fields
> that should be unique (related fields and USERNAME_FIELD specifically).
>
> I tried modifying `total_unique_constraints` and the only tests which
> failed were related to the above concern and
> `test_total_ordering_optimization_meta_constraints` which also uses `
> total_unique_constraints`. My application works fine and the validation
> error are correctly raised in my forms.
>
> The current behaviour of `Model.validate_unique` is also not the one I
> expected as my conditional `UniqueConstraint` were not used (which caused
> the integrity error).
>
> Am I missing something? Or should we use all constraints (including
> partial) in `Model.validate_unique`?
>
> If this is indeed what should be done, adding an `all_unique_constraints`
> next to `total_unique_constraints` and using it in `Model.validate_unique`
> instead of `total_unique_constraints` would do the trick. I don't mind
> opening a ticket and doing the PR if needed.
>
> Thanks.
>
--
You received this message because you are subscribed to the Google Groups
"Django developers (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-developers/4972f6d0-c590-473d-8571-063738baf2ccn%40googlegroups.com.