Can you spot the problem with this Django code?
from django.db import models
class ExampleModel(models.Model):
date = models.DateField()
name = models.CharField(unique_for_date='date')
It's unique_for_date
. unique_for_date
ensures name
will be unique for date
. So far so good, but unique_for_date
(and it's siblings unique_for_month
and unique_for_year
) has a lot of gotchas and is very brittle:
- The constraint is not enforced by the database.
- It is checked during
Model.validate_unique()
and so will not occur ifModel.save()
is called without first callingModel.validate_unique()
. - It won't be checked even if
Model.validate_unique()
is called when using aModelForm
if the form that form excludes any of the fields involved in the checks. - Only the date portion of the field will be considered, even when validating a
DateTimeField
.
That's a lot of "buuuuut" for a developer to keep in their minds when building a mental model. Lots of room there for the unexpected to creep in:
- If a developer does
ExampleModel.objects.create(...)
ad hoc in the shell and forgets to first callvalidate_unique()
. Sure we should not SSH into the production shell and create records ad hoc, but he without sin etc. - If a view or serializer does
ExampleModel.objects.create(...)
orModel.save()
without explicitly callingvalidate_unique()
. Yes, code review should catch it but if code reviewers could catch 100% of mistakes 100% of time with 100% consistency then we would not need code review at all because such Übermensch would not create bugs in the first place.
So unique_for
has many traps that can be triggered by human error. When implementing these fields the developer may conclude that these problems don't apply for the specific problem they're solving, and they trust themselves, their current and future team mates not to make mistakes. However, over time requirements changes. Over time things tend to get more different, not more similar. Code entropy is real. As the situation on the ground changes can we be sure that one of those problems won't be hit? What's your risk appetite?
Avoiding the problem
Instead of the terse but brittle:
from django.db import models
class ExampleModel(models.Model):
date = models.DateField()
name = models.CharField(unique_for_date='date')
We can do a more verbose, less DRY, but simultaneously more explicit and more future proof:
class MyModel(models.Model):
date = models.DateField()
name = models.CharField()
def save(self, *args, **kwargs):
# change specific filter depending on need.
if MyModel.objects.filter(date=self.date, name=self.name).exists():
raise ValidationError({'name': 'Nein!'})
return super().save(*args, **kwargs)
This validation will be called whenever Model.save()
is called, but unfortunately not when Model.objects.update()
is called, but this is about harm reduction rather than perfection.
Does your codebase use unique_for
?
It’s easy for tech debt to slip in. I can check that for you at django.doctor, or can review your GitHub PRs:
Or try out Django refactor challenges.
Top comments (2)
Great article, but I think you may have to use the actual class name instead of the instance.
stackoverflow.com/a/3874563/3278083
thanks, fixed