The hidden pitfalls of unique_for_date
--
Can you spot the maintainability problem with this code?
from django.db import modelsclass MyModel(models.Model):
date = models.DateField()
text = models.CharField(unique_for_date='date')
unique_for_date
is the culprit. It’s meant to make sure text
will be unique for date
. However, unique_for_date
has a pitfalls:
- It’s checked only if
MyModel.validate_unique()
is called, so ifMyModel.save()
is called without first callingMyModel.validate_unique()
then you’re not going to have a great time. - It won’t be checked even if
Model.validate_unique()
is called when using a ModelForm if the form that does no include a field involved in the check. - Only the date portion of the field will be considered, even when validating a
DateTimeField
. - The constraint is not enforced by the database.
That’s a lot of caveats to keep in mind when building a mental model of the code we’re working on. Lots of room there for the unexpected to creep in:
- If a developer does
Myodel.objects.create(…)
in the shell and forgets to first callvalidate_unique()
. Yes we should not SSH into the production shell and create records ad hoc, but most people have done it at least once. - If a view or serializer does
MyModel.objects.create(…)
orModel.save()
without callingvalidate_unique()
. Code review can catch this but if code humans could catch 100% of mistakes 100% of time with 100% consistency then we wouldn’t need code review in the first place because such Übermensch would not create bugs in the first place.
So unique_for
has many pitfalls to be triggered by human error. When implementing the 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 nice and small but brittle:
from django.db import modelsclass MyModel(models.Model):
date = models.DateField()
text = models.CharField(unique_for_date=’date’)
We can do a more verbose, less DRY, but simultaneously more explicit and more future proof:
class ExampleModel(models.Model):
date = models.DateField()
text= models.CharField()def save(self, *args, **kwargs):
# change specific filter depending on need.
if self.objects.filter(date=self.date, text=self.text).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 there’s no silver bullet here.
Does your codebase use `unique_for`?
It’s easy for tech debt to slip in. I can check that for you at django.doctor. I’m a GitHub bot that suggest Django improvements to your code:
If you would prefer code smells not make it into your codebase, I also review pull requests:
See the GitHub PR bot and reduce dev effort of improving your code.