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个想法:
  • 想法#1 :使用unique_together->不适用于[在文档中] M2M字段
    (https://docs.djangoproject.com/en/2.1/ref/models/options/#unique-together)
  • 想法#2 :覆盖save()方法。不适用于M2M,因为在调用addremove方法时,不会调用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()

    谢谢!

    最佳答案

    我在这里看到两个单独的问题:

  • 您想对数据实现特定的约束。
  • 如果违反了约束,则要还原以前的操作。特别是,如果您在违反约束的同一请求中添加了任何Badge,则要还原Restaurants实例的创建。

  • 关于1,您的约束很复杂,因为它涉及多个表。这就排除了数据库约束(或者,您可以通过触发器来做到)或简单的模型级验证。

    上面的代码显然可以有效地防止adds违反约束。但是请注意,如果更改了现有Badge的标识符,也可能会违反此约束。想必您也想防止这种情况吗?如果是这样,您需要向Badge添加类似的验证(例如Badge.clean()中)。

    关于2,如果希望在违反约束时还原Badge实例的创建,则需要确保将操作包装在数据库事务中。您尚未告诉我们这些对象区域的创建 View (自定义 View ?Django admin?),因此很难给出具体建议。本质上,您希望拥有:
    with transaction.atomic():
        badge_instance.save()
        badge_instance.add(...)
    

    如果这样做,M2M pre_add信号引发的异常将回滚事务,并且您不会在数据库中得到剩余的Badge。请注意,默认情况下,管理员 View 是在事务中运行的,因此,如果您使用的是管理员,则应该已经发生了。

    另一种方法是在创建Badge对象之前进行验证。有关在Django管理员中使用ModelForm验证的信息,请参见this answer

    关于带有M2M字段的Django对象唯一性 hell ,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/52147255/

    10-09 01:12