So I have some models that look like this
class Person(BaseModel): name = models.CharField(max_length=50) # other fields declared here... friends = models.ManyToManyField( to="self", through="Friendship", related_name="friends_to", symmetrical=True ) class Friendship(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) So basically, I'm trying to model a Facebook-like friends situation, where there are different persons and one can ask any other person to be friends. That relationship is expressed through this last model Friendship.
So far so good. But there are three situations I'd like to avoid:
-
- A Friendship can't have the same person in the
friend_fromandfriend_tofields
- A Friendship can't have the same person in the
-
- Only one
Friendshipfor a set of two Friends should be allowed.
- Only one
The closest I've got to that, is adding this under the Friendship model:
class Meta: 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")), ) ] This totally solves situation 1, avoiding someone to befriend with himself, using the CheckConstraint. And partially solves situation number 2, because it avoids having two Friendships like this:
p1 = Person.objects.create(name="Foo") p2 = Person.objects.create(name="Bar") Friendship.objects.create(friend_from=p1, friend_to=p2) # This one gets created OK Friendship.objects.create(friend_from=p1, friend_to=p2) # This one fails and raises an IntegrityError, which is perfect Now there's one case that'd like to avoid that still can happen:
Friendship.objects.create(friend_from=p1, friend_to=p2) # This one gets created OK Friendship.objects.create(friend_from=p2, friend_to=p1) # This one won't fail, but I'd want to How would I make the UniqueConstraint work in this "two directions"? Or how could I add another constraint to cover this case?
Of course, I could overwrite the save method for the model or enforce this in some other way, but I'm curious about how this should be done at the database level.
symmetrical=Trueand create the reverse duplicate yourself.savemethod. That said, I'm still curious because I'm almost sure it could be done with theUniqueConstraintand aQoperator, I'm just not finding the how myself.person.friends.all()) or making a query, we want all objects whether they are referred one way or the other, so then we would need to complicate these queries by adding more conditions, etc. Although the reverse duplication may be somewhat redundant it is still somewhat acceptable for efficiency in retrieving instances.UniqueConstraints (Django docs) are going to support expressions, so you can write some expression that concatenates the two fields with some separator and in some order, effectively achieving what you want.