Django 在两个方向上唯一的共同约束

问题描述

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

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,related_name="friendships_to")
    state = models.CharField(
        max_length=20,choices=FriendshipState.choices,default=FriendshipState.pending)

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

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

    1. 友谊不能在 friend_fromfriend_to 字段中包含同一个
    1. 对于一组两个朋友,只允许一个 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 方法或以其他方式强制执行此操作,但我很好奇应如何在数据库级别执行此操作。

解决方法

所以,总结一下评论中的讨论,并为其他研究同一问题的人提供一个例子:

  • 与我的看法相反,正如 @Abdul Aziz Barkat 所指出的那样,在今天,使用 Django 3.2.3 无法实现这一点。 UniqueConstraint 目前支持的 The condition kwarg 还不足以使这项工作发挥作用,因为它只是使约束成为条件,但不能将其扩展到其他情况。

  • 正如 @Abdul Aziz Barkat 所评论的那样,未来可能会使用 UniqueConstraint 对 expressions 的支持。

  • 最后,使用模型中的自定义 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,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类(即M2M关系的“通过”表):

    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)

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

  • 我不确定使用 Model 类中的 save 方法是实现此目的的最佳方法,因为在某些情况下甚至没有调用 save,特别是在使用 {{3 }}。

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

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

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