limit number of foreign key using Django CheckConstraint

Question:

I am using Django 4.1 and Postgresql and as stated in their documentation CheckConstraint accept Q object and Expression.

Based on https://code.djangoproject.com/ticket/31646, I thought my solution would work, but when calling makemigrations nothing happens (Count inherit from Func).

Goal: I would like to limit the number of Messages per Discussion.

I did see a solution using a validators on the ForeignKey field but it is not robust (see Limit number of foreign keys). I would prefer not to have to create a SQL function and calling it (would like a Django solution only).

from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.lookups import IntegerLessThan

class Discussion(models.Model):
    MAX_MESSAGES = 10


class Message(models.Model):
    discussion = models.ForeignKey(
        "discussion.Discussion",
        models.CASCADE,
        related_name="messages",
    )
    constraints = [
        models.CheckConstraint(
            name="limit_discussion_messages",
            check=IntegerLessThan(
                models.Count("discussion", filter=models.Q(discussion=models.F("discussion"))),
                models.Value(Discussion.MAX_MESSAGES),
            ),
        ),
    ]

EDIT:
After some discussion in the comments and reading provided anwser here are my findings:
Perfect constraint (without data race) cannot be achieved with Django and no raw SQL in it’s current state (4.1).

Using a Django solution only and it’s validators system, you have two choices:

  • Check before saving and accept the fact that you might have a few more records than the allowed limit
  • Check after saving and accept the fact that you might rollback more operation than necessary, but allowed limit will be enforced
Asked By: Barbeuk

||

Answers:

constraints need to be in the Meta class(look here):

class Message(models.Model):
    discussion = models.ForeignKey(
        "discussion.Discussion",
        models.CASCADE,
        related_name="messages",
    )

    class Meta:
        constraints = [
        models.CheckConstraint(
        name="limit_discussion_messages",
        check=IntegerLessThan(
            models.Count("discussion", filter=models.Q(discussion=models.F("discussion"))),
            models.Value(Discussion.MAX_MESSAGES),
        ),
        ),
    ]

Other solutions:

Solution 1:

Each model has a save() option that is called when the model is saved. You can check here and raise an error if the user already has 10 messages.

class Message(models.Model):
    discussion = models.ForeignKey("discussion.Discussion", models.CASCADE,related_name="messages")

    def save(self,*args, **kwargs):
        if self.id == None: #Creating a new object
            if Message.objects.filter(discussion=request.discussion).count() >= 10:
                #Raise whatever error you want or just return false
        return super(Post, self).save(*args, **kwargs)

Solution 2:

Using validators:

def restrict_amount(value):
        if Message.objects.filter(discussion=value).count() >= 3:
            raise ValidationError('You already have max amount of discussions (3)')
    
    class Message(models.Model):
        discussion = models.ForeignKey("discussion.Discussion",validators=(restrict_amount,))
Answered By: Shahab Rahnama
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.