The hidden pitfalls of unique_for_date

Code Review Doctor
2 min readNov 7, 2020

--

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 if MyModel.save() is called without first calling MyModel.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 call validate_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(…) or Model.save() without calling validate_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.

--

--