【问题标题】:Django unique together constraint in two directionsDjango唯一一起约束两个方向
【发布时间】:2021-08-05 08:32:08
【问题描述】:

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

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 来表达。

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

    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 方法或以其他方式强制执行此操作,但我很好奇这应该如何在数据库级别完成。

【问题讨论】:

  • 这看起来与stackoverflow.com/questions/67540575/… 非常相似?无论如何,当您设置 symmetrical=True 并自己创建反向副本时,您可以做 Django 所做的事情。
  • @AbdulAzizBarkat 嘿,是的,这几乎是完全相同的问题。老实说,除了在自定义 save 方法中强制执行约束之外,我没有想过自己创建反向副本的可能性,这可能是另一种选择。话虽如此,我仍然很好奇,因为我几乎可以肯定它可以通过 UniqueConstraintQoperator 来完成,我只是没有找到自己的方法。
  • 创建反向副本是这里的最佳选择(实际上这也是 Django 正在做的事情)。这是因为如果关系是对称的,那么在尝试获取相关对象 (person.friends.all()) 或进行查询时,我们希望所有对象都以一种或另一种方式引用,所以我们需要使这些复杂化通过添加更多条件等进行查询。虽然反向复制可能有些多余,但在检索实例的效率方面仍然可以接受。
  • 你想要的虽然不能在当前版本的 Django 中完成,但在未来的某些版本中 UniqueConstraints (Django docs) 将支持表达式,因此你可以编写一些表达式将两个字段与一些分隔符连接起来并以某种顺序有效地实现您想要的。

标签: python-3.x django django-models unique-constraint


【解决方案1】:

所以,只是总结一下 cmets 中的讨论,并为其他研究相同问题的人提供一个示例:

  • 与我的看法相反,正如 @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, 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类(即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)

关于最后一个 sn-p 的几点说明:

  • 我不确定使用 Model 类中的 save 方法是实现此目的的最佳方法,因为在某些情况下甚至不调用 save ,尤其是在使用 bulk_create 时。

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

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

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

【讨论】:

    猜你喜欢
    • 2013-07-04
    • 2019-09-25
    • 1970-01-01
    • 2019-04-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多