Django Unique Together Constraint In Two Directions
Solution 1:
So, just to sum up the discussion in the comments and to provide an example for anyone else looking into the same problem:
Contrary to my belief, this can not be achieved as of today, with Django 3.2.3, as pointed out by @Abdul Aziz Barkat. The
condition
kwarg that UniqueConstraint supports today isn't enough to make this work, because it just makes the constraint conditional, but it can't extend it to other cases.The way of doing this in the future probably will be with UniqueConstraint's support for expressions, as commented by @Abdul Aziz Barkat too.
Finally, one way of solving this with a custom
save
method in the model could be:
Having this situation as posted in the question:
classPerson(BaseModel):
name = models.CharField(max_length=50)
# other fields declared here...
friends = models.ManyToManyField(
to="self",
through="Friendship",
related_name="friends_to",
symmetrical=True
)
classFriendship(BaseModel):
friend_from = models.ForeignKey(
Person, on_delete=models.CASCADE, related_name="friendships_from")
friend_to = models.ForeignKey(
Person, on_delete=models.CASCADE, related_name="friendships_to")
state = models.CharField(
max_length=20, choices=FriendshipState.choices, default=FriendshipState.pending)
classMeta:
constraints = [
constraints.UniqueConstraint(
fields=['friend_from', 'friend_to'], name="unique_friendship_reverse"
),
models.CheckConstraint(
name="prevent_self_follow",
check=~models.Q(friend_from=models.F("friend_to")),
)
]
Add this to the Friendship
class (that is, the "through" table of the M2M relationship):
defsave(self, *args, symmetric=True, **kwargs):
ifnot self.pk:
if symmetric:
f = Friendship(friend_from=self.friend_to,
friend_to=self.friend_from, state=self.state)
f.save(symmetric=False)
else:
if symmetric:
f = Friendship.objects.get(
friend_from=self.friend_to, friend_to=self.friend_from)
f.state = self.state
f.save(symmetric=False)
returnsuper().save(*args, **kwargs)
A couple of notes on that last snippet:
I'm not sure that using the
save
method from the Model class is the best way to achieve this because there are some cases where save isn't even called, notably when usingbulk_create
.Notice that I'm first checking for
self.pk
. This is to identify when we are creating a record as opposed to updating a record.If we are updating, then we will have to perform the same changes in the inverse relationship than in this one, to keep them in sync.
If we are creating, notice how we are not doing
Friendship.objects.create()
because that would trigger a RecursionError - maximum recursion depth exceeded. That's because, when creating the inverse relationship, it will also try to create its inverse relationship, and that one also will try, and so on. To solve this, we added the kwarg symmetric to the save method. So when we are calling it manually to create the inverse relationship, it doesn't trigger any more creations. That's why we have to first create aFriendship
object and then separately call thesave
method passingsymmetric=False
.
Post a Comment for "Django Unique Together Constraint In Two Directions"