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

所以基本上,我试图模拟一个类似Facebook的朋友情况,有不同的人,一个人可以邀请任何其他人成为朋友。这种关系通过最后一个模型CCD_ 1来表达。

到目前为止还不错但有三种情况我想避免:

    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目前支持的Friendship0Kwarg不足以实现这一点,因为它只是使约束成为条件约束,但不能将其扩展到其他情况。

  • 正如@Abdul-Aziz-Barkat所评论的那样,未来实现这一点的方法可能是UniqueConstraint对表达式的支持。

  • 最后,在模型中使用自定义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)

关于最后一个片段的几个注意事项:

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

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

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

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

最新更新