Django unique together 约束在两个方向
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)
所以基本上,我正在尝试模拟一种类似 Facebook 的朋友情况,其中有不同的人,并且可以要求任何其他人成为朋友。这种关系通过最后一个模型表达 Friendship
.
到目前为止一切顺利。 但是我想避免三种情况:
-
- 友谊在
friend_from
和 friend_to
字段中不能有同一个人
-
- 一组两个朋友只允许一个
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
方法或以其他方式强制执行此方法,但我很好奇应该如何在数据库级别完成此操作。
因此,总结一下评论中的讨论,并为其他研究相同问题的人提供示例:
与我的看法相反,这在今天无法实现,使用 Django 3.2.3,正如 @ 所指出的阿卜杜勒·阿齐兹·巴卡特。 The condition
kwarg 今天支持的 UniqueConstraint 不足以使这项工作生效,因为它只是使约束成为条件,但不能将其扩展到其他情况。
未来这样做的方式可能是 UniqueConstraint 对 expressions 的支持,正如 @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")),
)
]
将此添加到 Friendship
class(即 M2M 关系的“通过”table):
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)
关于最后一个片段的几点说明:
我不确定使用模型 class 中的 save
方法是否是实现此目的的最佳方法,因为在某些情况下甚至无法保存调用,特别是在使用 bulk_create
.
时
请注意,我首先检查 self.pk
。这是为了确定我们何时 创建 记录而不是 更新 记录。
如果我们正在更新,那么我们将不得不在反向关系中执行与此相同的更改,以保持它们同步。
如果我们正在创建,请注意我们没有做的事情 Friendship.objects.create()
因为那会触发 RecursionError - 最大递归深度超出 。那是因为,在创建反向关系的时候,它也会尝试创建它的反向关系,那个也会尝试,等等。为了解决这个问题,我们在保存方法中添加了 kwarg symmetric。所以当我们手动调用它来创建反向关系时,它不会触发更多的创建。这就是为什么我们要先创建一个Friendship
对象,然后单独调用save
方法传递symmetric=False
.
所以我有一些模型看起来像这样
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
.
到目前为止一切顺利。 但是我想避免三种情况:
-
- 友谊在
friend_from
和friend_to
字段中不能有同一个人
- 友谊在
-
- 一组两个朋友只允许一个
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
方法或以其他方式强制执行此方法,但我很好奇应该如何在数据库级别完成此操作。
因此,总结一下评论中的讨论,并为其他研究相同问题的人提供示例:
与我的看法相反,这在今天无法实现,使用 Django 3.2.3,正如 @ 所指出的阿卜杜勒·阿齐兹·巴卡特。 The
condition
kwarg 今天支持的 UniqueConstraint 不足以使这项工作生效,因为它只是使约束成为条件,但不能将其扩展到其他情况。未来这样做的方式可能是 UniqueConstraint 对 expressions 的支持,正如 @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")),
)
]
将此添加到 Friendship
class(即 M2M 关系的“通过”table):
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)
关于最后一个片段的几点说明:
我不确定使用模型 class 中的
时save
方法是否是实现此目的的最佳方法,因为在某些情况下甚至无法保存调用,特别是在使用bulk_create
.请注意,我首先检查
self.pk
。这是为了确定我们何时 创建 记录而不是 更新 记录。如果我们正在更新,那么我们将不得不在反向关系中执行与此相同的更改,以保持它们同步。
如果我们正在创建,请注意我们没有做的事情
Friendship.objects.create()
因为那会触发 RecursionError - 最大递归深度超出 。那是因为,在创建反向关系的时候,它也会尝试创建它的反向关系,那个也会尝试,等等。为了解决这个问题,我们在保存方法中添加了 kwarg symmetric。所以当我们手动调用它来创建反向关系时,它不会触发更多的创建。这就是为什么我们要先创建一个Friendship
对象,然后单独调用save
方法传递symmetric=False
.