Categories
Python

Django: Deferred constrain enforcement

Another Friday, another Django related post. I guess this blog is becoming a bit monothematic. I promise the next ones will bring the much-needed diversity of contents, but today let’s explore a very useful feature of the Django’s ORM.

Ok… Ok… it’s more of a feature of PostgreSQL that Django supports, and it isn’t available on the other database backends. But let’s dive in any way.

Let’s imagine this incredibly simplistic scenario where you have the following model:

class Player(models.Model):
  team = models.ForeignKey(Team, on_delete=models.CASCADE)
  squad_number = models.PositiveSmallIntegerField()

  class Meta:
    constrains = [
      models.UniqueConstraint(
        name="unique_squad_number",
        fields=["team", "squad_number"],
      )
    ]

So a team has many players and each player has a different squad/shirt number. Only one player can use that number for a given team.

Users, can select their teams and then re-arrange their player’s numbers however they like. To keep it simple, let’s assume it is done through the Django Admin, using a Player Inline on the Team‘s model admin.

We add proper form validation, to ensure that no players in the submitted squad are assigned the same squad_number. Things work great until you start noticing that despite your validation and despite the user’s input not having any players assigned the same number, integrity errors are flying around. What’s happening?

Well, when the system tries to update some player records after being correctly validated, each update/insertion is checked against the constraint (even atomically within a transaction). This means that the order of the updates, or in certain situations all updates with correct data, will “raise” integrity errors, due to conflicts with the data currently stored in the database.

The solution? Deferring the integrity checks to the end of the transaction. Here’s how:

class Player(models.Model):
  team = models.ForeignKey(Team, on_delete=models.CASCADE)
  squad_number = models.PositiveSmallIntegerField()

  class Meta:
    constrains = [
      models.UniqueConstraint(
        name="unique_squad_number",
        fields=["team", "squad_number"],
        deferrable=models.Deferrable.DEFERRED,
      )
    ]

Now, when you save multiple objects within a single transaction, you will no longer see those errors if the input data is valid.

2 replies on “Django: Deferred constrain enforcement”

@content Thank you so much for the tip and the post! I was missing a good quick example of the issue – would this fail with the normal constraint?

“`
with transaction.atomic():
object1 = Player.objects.get(team__name="Barca", number=1)
object1.number = 2 # Was 1
object1.save()

object2 = Player.objects.get(team__name="Barca", number=2)
object2.number = 1 # Was 2
object2.save() # Raises IntegrityError?
“`

Likes

Reposts

Mentions

  • Gonçalo Valério
  • Benjamin Balder Bach

Leave a Reply

Your email address will not be published. Required fields are marked *