[英]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)
所以基本上,我正在尝试 model 类似 Facebook 的朋友情况,其中有不同的人,可以要求任何其他人成为朋友。 这种关系通过最后的 model Friendship
来表达。
到目前为止,一切都很好。 但是我想避免三种情况:
friend_from
和friend_to
字段中包含同一个人Friendship
。 我最接近的,是在Friendship
model 下添加这个:
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
在这个“两个方向”上工作? 或者我如何添加另一个约束来涵盖这种情况?
当然,我可以覆盖 model 的save
方法或以其他方式强制执行此操作,但我很好奇这应该如何在数据库级别完成。
所以,只是为了总结评论中的讨论,并为其他研究同样问题的人提供一个例子:
与我的看法相反,正如@Abdul Aziz Barkat所指出的那样,截至今天,使用 Django 3.2.3 无法实现这一点。 UniqueConstraint 今天支持的condition
kwarg不足以完成这项工作,因为它只是使约束条件化,但不能将其扩展到其他情况。
正如@Abdul Aziz Barkat所评论的那样,未来这样做的方式可能是UniqueConstraint 对表达式的支持。
最后,使用 model 中的自定义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 关系的“通过”表)中:
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 class 中的save
方法是实现此目的的最佳方法,因为在某些情况下甚至不调用 save ,尤其是在使用bulk_create
时。
请注意,我首先检查self.pk
。 这是为了确定我们何时创建记录而不是更新记录。
如果我们正在更新,那么我们将不得不在反向关系中执行与此相同的更改,以使它们保持同步。
如果我们正在创建,请注意我们没有在做Friendship.objects.create()
因为这会触发RecursionError - maximum recursion depth exceeded 。 那是因为,在创建反向关系时,它也会尝试创建它的反向关系,并且它也会尝试,以此类推。 为了解决这个问题,我们将 kwarg对称添加到 save 方法中。 因此,当我们手动调用它来创建反向关系时,它不会触发任何更多的创建。 这就是为什么我们必须首先创建一个Friendship
object 然后单独调用save
方法传递symmetric=False
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.