class Badge(SafeDeleteModel):
owner = models.ForeignKey(settings.AUTH_USER_MODEL,
blank=True, null=True,
on_delete=models.PROTECT)
restaurants = models.ManyToManyField(Restaurant)
identifier = models.CharField(max_length=2048) # not unique at a DB level!
我想确保对于给定餐厅的任何徽章,它必须具有唯一的标识符。这是我的4个想法:
unique_together
->不适用于[文档中所述的M2M字段
(https://docs.djangoproject.com/en/2.1/ref/models/options/#unique-together)save()
方法。无法完全与M2M配合使用,因为在调用add
或remove
方法时,不会调用save()
。 想法#3 :使用显式through
模型,但是由于我生活在生产环境中,因此我想避免冒险迁移诸如此类的重要结构。 编辑:在考虑之后,我看不出它实际上有什么帮助。
想法4 :在调用m2m_changed
方法时,使用add()
信号检查唯一性。
我最终想到了想法4 ,并认为一切正常,并发出了这个信号...
@receiver(m2m_changed, sender=Badge.restaurants.through)
def check_uniqueness(sender, **kwargs):
badge = kwargs.get('instance', None)
action = kwargs.get('action', None)
restaurant_pks = kwargs.get('pk_set', None)
if action == 'pre_add':
for restaurant_pk in restaurant_pks:
if Badge.objects.filter(identifier=badge.identifier).filter(restaurants=restaurant_pk):
raise BadgeNotUnique(MSG_BADGE_NOT_UNIQUE.format(
identifier=badge.identifier,
restaurant=Restaurant.objects.get(pk=restaurant_pk)
))
...直到今天,当我在数据库中找到许多具有相同标识符但没有餐厅的徽章(在业务级别上不应发生)
我了解到save()
与信号之间没有无原子性。
这意味着,如果用户在尝试创建徽章时遇到关于唯一性的错误,则会创建该徽章,但不会链接任何饭店。
因此,问题是:如何在模型级别确保 ,如果信号引发错误,则save()
不会提交?
谢谢!
答案 0 :(得分:2)
我在这里看到两个单独的问题:
您要对数据实施特定的约束。
如果违反了约束,则要还原以前的操作。特别是,如果在违反约束的同一请求中添加了任何Badge
,则您想还原Restaurants
实例的创建。
关于1,您的约束很复杂,因为它涉及多个表。这样就排除了数据库约束(或者,您可以通过触发器来做到)或简单的模型级验证。
您的上述代码显然可以有效地防止违反约束的adds
。但是请注意,如果更改现有Badge
的标识符,也可能会违反此约束。想必您也想防止这种情况吗?如果是这样,则需要向Badge
添加类似的验证(例如,在Badge.clean()
中)。
关于2,如果希望在违反约束时还原Badge
实例的创建,则需要确保将操作包装在数据库事务中。您尚未告诉我们这些对象区域的创建视图(自定义视图?Django admin?),因此很难给出具体建议。本质上,您想拥有这个:
with transaction.atomic():
badge_instance.save()
badge_instance.add(...)
如果这样做,则M2M pre_add
信号引发的异常将回滚事务,并且您不会在数据库中得到剩余的Badge
。请注意,默认情况下,管理员视图是在事务中运行的,因此,如果您使用的是管理员,则应该已经发生了。
另一种方法是在创建Badge
对象之前进行验证。有关在Django管理员中使用ModelForm
验证的信息,请参见this answer。
答案 1 :(得分:0)
您可以为M2M模型指定your own connecting model,然后在成员资格模型的元类中添加unique_together
约束
class Badge(SafeDeleteModel):
...
restaurants = models.ManyToManyField(Restaurant, through='BadgeMembership')
class BadgeMembership(models.Model):
restaurant = models.ForeignKey(Restaurant, null=False, blank=False, on_delete=models.CASCADE)
badge = models.ForeignKey(Badge, null=False, blank=False, on_delete=models.CASCADE)
class Meta:
unique_together = (("restaurant", "badge"),)
这将创建一个介于Badge
和Restaurant
之间的对象,该对象对于每个餐厅的每个徽章都是唯一的。
您还可以添加自定义save
函数,您可以在其中手动检查唯一性。这样,您可以手动引发异常。
class BadgeMembership(models.Model):
restaurant = models.ForeignKey(Restaurant, null=False, blank=False, on_delete=models.CASCADE)
badge = models.ForeignKey(Badge, null=False, blank=False, on_delete=models.CASCADE)
def save(self, *args, **kwargs):
# Only save if the object is new, updating won't do anything
if self.pk is None:
membershipCount = BadgeMembership.objects.filter(
Q(restaurant=self.restaurant) &
Q(badge=self.badge)
).count()
if membershipCount > 0:
raise BadgeNotUnique(...);
super(BadgeMembership, self).save(*args, **kwargs)
答案 2 :(得分:0)
恐怕实现此目标的正确方法实际上是通过调整“通过”模型。但是请记住,在数据库级别,此“直通”模型已经存在,因此,您的迁移将只是添加唯一的约束。这是一个相当简单的操作,并且实际上并没有涉及任何实际的迁移,我们经常在生产环境中进行此操作。
看看this example,它几乎汇总了您需要的所有内容。