代码之家  ›  专栏  ›  技术社区  ›  David Dahan

Django对象与M2M字段

  •  8
  • David Dahan  · 技术社区  · 6 年前
    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!
    

    • unique_together ->不适用于[文档中]所述的M2M字段 ( https://docs.djangoproject.com/en/2.1/ref/models/options/#unique-together )
    • 想法2 :覆盖 save() 方法。不能完全与M2M配合使用,因为在调用 add remove 方法, 不调用。
    • 想法#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)
                    ))
    

    ……直到今天,我在数据库中发现了许多具有相同标识的徽章,但没有餐厅(在商业层面不应该发生) 我知道有 无原子性 保存() 这意味着,如果用户在尝试创建徽章时出现关于唯一性的错误,则会创建徽章,但没有与其链接。

    所以,问题是:你如何确保 在模型级别 如果信号出现错误 保存() 没有承诺?

    3 回复  |  直到 6 年前
        1
  •  3
  •   Kevin Christopher Henry    6 年前

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

    1. 您希望对数据实施特定约束。

    2. 如果违反了约束,则要还原以前的操作。特别是,您希望恢复 Badge Restaurants

    你上面的代码显然能有效防止 adds 违反了约束。但是,请注意,如果现有 徽章 Badge.clean() ).

    关于2,如果您想创建 徽章 要在违反约束时还原的实例,需要确保操作包装在数据库事务中。您还没有告诉我们这些对象区域创建的视图(自定义视图?Django管理员?)所以很难给出具体的建议。基本上,您希望:

    with transaction.atomic():
        badge_instance.save()
        badge_instance.add(...)
    

    如果这样做,M2M会引发一个异常 pre_add signal将回滚事务,而您不会得到剩余的 徽章 在你的数据库里。请注意,默认情况下,管理视图在事务中运行,因此如果您使用的是管理视图,则应该已经发生了这种情况。

    另一种方法是在测试之前进行验证 徽章 this answer 关于使用 ModelForm

        2
  •  1
  •   Anoyz    6 年前

    恐怕实现这一目标的正确方法真的是采用“通过”模式。但请记住,在数据库级别,这个“through”模型已经存在,因此您的迁移只是添加一个惟一的约束。这是一个相当简单的操作,实际上并不涉及任何真正的迁移,我们通常在生产环境中进行。

    this example ,它几乎是你需要的所有东西的总和。

        3
  •  1
  •   Johan    6 年前

    您可以指定 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

    Badge and restaurant membership

    可选:保存检查

    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)