Django使用M2M字段来对抗唯一性

Dav*_* D. 8 django django-signals django-models

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!
Run Code Online (Sandbox Code Playgroud)

我想确保对于任何徽章,对于给定的餐馆,它必须具有唯一的标识符.以下是我的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_changedadd()调用方法的任何时候使用信号检查唯一性.

我结束了这个想法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)
                ))
Run Code Online (Sandbox Code Playgroud)

...直到今天,当我在我的数据库中发现许多具有相同标识符但没有餐厅的徽章(不应该在业务层面发生)时,我知道信号和信号之间没有原子性save().这意味着,如果用户在尝试创建徽章时出现关于唯一性的错误,则会创建徽章,但不会将餐馆链接到该徽章.

所以,问题是:如何在模型级别确保如果信号引发错误,save()则不会提交?

谢谢!

Kev*_*nry 3

我在这里看到两个不同的问题:

  1. 您想要对数据实施特定的约束。

  2. 如果违反约束,您希望恢复以前的操作。特别是,Badge如果Restaurants在同一请求中添加了任何违反约束的实例,您希望恢复实例的创建。

关于1,你的约束很复杂,因为它涉及多个表。这排除了数据库约束(好吧,您可能可以使用触发器来完成)或简单的模型级验证。

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

关于2,如果您希望在违反约束时恢复实例的创建Badge,则需要确保操作包装在数据库事务中。您还没有告诉我们这些对象区域创建的视图(自定义视图?Django admin?),因此很难给出具体的建议。本质上,你想要这样:

with transaction.atomic():
    badge_instance.save()
    badge_instance.add(...)
Run Code Online (Sandbox Code Playgroud)

如果这样做,M2M 信号引发的异常pre_add将回滚事务,并且您将不会Badge在数据库中获得剩余内容。请注意,管理视图默认在事务中运行,因此如果您正在使用管理,这应该已经发生。

Badge另一种方法是在创建对象之前进行验证。例如,请参阅有关在 Django 管理中使用验证的答案。ModelForm