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

Ger*_*cer 5 django django-models unique-constraint python-3.x

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

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)
Run Code Online (Sandbox Code Playgroud)

基本上,我正在尝试模拟一种类似 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")),
        )
    ]
Run Code Online (Sandbox Code Playgroud)

这完全解决了情况1,避免某人与自己交朋友,使用CheckConstraint. 并部分解决了第二种情况,因为它避免了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 
Run Code Online (Sandbox Code Playgroud)

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

 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
Run Code Online (Sandbox Code Playgroud)

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

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

Ger*_*cer 4

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

  • 与我的信念相反,正如@Abdul Aziz Barkat指出的那样,从今天起,使用 Django 3.2.3 还无法实现这一点UniqueConstraint 目前支持的kwarg不足以实现此目的,因为它只是使约束成为有条件的,但无法将其扩展到其他情况condition

  • 将来执行此操作的方法可能是使用 UniqueConstraint 对表达式的支持,正如@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")),
            )
        ]

Run Code Online (Sandbox Code Playgroud)

将其添加到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)
Run Code Online (Sandbox Code Playgroud)

关于最后一个片段的一些注释:

  • 我不确定使用saveModel 类中的方法是实现此目的的最佳方法,因为在某些情况下甚至不调用 save ,特别是在使用bulk_create.

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

  • 如果我们要更新,那么我们将必须在逆关系中执行与此关系相同的更改,以保持它们同步。

  • 如果我们正在创建,请注意我们没有这样做,Friendship.objects.create()因为这会触发RecursionError - Maximum recursion Deepisodeed。这是因为,当创建逆关系时,它也会尝试创建它的逆关系,并且那个人也会尝试,等等。为了解决这个问题,我们将 kwarg对称添加到 save 方法中。因此,当我们手动调用它来创建逆关系时,它不会触发更多的创建。这就是为什么我们要先创建一个Friendship对象,然后单独调用save方法传递symmetric=False