Django unique together 约束在两个方向

Django unique together constraint in two directions

所以我有一些模型看起来像这样

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)

所以基本上,我正在尝试模拟一种类似 Facebook 的朋友情况,其中有不同的人,并且可以要求任何其他人成为朋友。这种关系通过最后一个模型表达 Friendship.

到目前为止一切顺利。 但是我想避免三种情况:

我最接近的是在 Friendship 模型下添加这个:

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")),
        )
    ]

这完全解决了情况1,避免别人与自己交朋友,使用CheckConstraint。 并部分解决了情况 2,因为它避免了像这样有两个 Friendships

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 

现在有一种情况希望避免这种情况仍然会发生:

 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

我如何使 UniqueConstraint 在这个“两个方向”上工作?或者我如何添加另一个约束来涵盖这种情况?

当然,我可以覆盖模型的 save 方法或以其他方式强制执行此方法,但我很好奇应该如何在数据库级别完成此操作。

因此,总结一下评论中的讨论,并为其他研究相同问题的人提供示例:

  • 与我的看法相反,这在今天无法实现,使用 Django 3.2.3,正如 @ 所指出的阿卜杜勒·阿齐兹·巴卡特The condition kwarg 今天支持的 UniqueConstraint 不足以使这项工作生效,因为它只是使约束成为条件,但不能将其扩展到其他情况。

  • 未来这样做的方式可能是 UniqueConstraint 对 expressions 的支持,正如 @Abdul Aziz Barkat 所评论的那样。

  • 最后,在模型中使用自定义 save 方法解决此问题的一种方法可能是:

问题中出现这种情况:

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)

    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")),
            )
        ]

将此添加到 Friendship class(即 M2M 关系的“通过”table):

    def save(self, *args, symmetric=True, **kwargs):
        if not 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)
        return super().save(*args, **kwargs)

关于最后一个片段的几点说明:

  • 我不确定使用模型 class 中的 save 方法是否是实现此目的的最佳方法,因为在某些情况下甚至无法保存调用,特别是在使用 bulk_create.

  • 请注意,我首先检查 self.pk。这是为了确定我们何时 创建 记录而不是 更新 记录。

  • 如果我们正在更新,那么我们将不得不在反向关系中执行与此相同的更改,以保持它们同步。

  • 如果我们正在创建,请注意我们没有做的事情 Friendship.objects.create() 因为那会触发 RecursionError - 最大递归深度超出 。那是因为,在创建反向关系的时候,它也会尝试创建它的反向关系,那个也会尝试,等等。为了解决这个问题,我们在保存方法中添加了 kwarg symmetric。所以当我们手动调用它来创建反向关系时,它不会触发更多的创建。这就是为什么我们要先创建一个Friendship对象,然后单独调用save方法传递symmetric=False.