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

时间:2022-09-11 18:26:14
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!

I want to ensure that for any badge, for a given restaurant, it must have a unique identifier. Here are the 4 ideas I have had:

我想确保对于任何徽章,对于给定的餐馆,它必须具有唯一的标识符。以下是我的4个想法:

  • idea #1: using unique_together -> Does not work with M2M fields as explained [in documentation] (https://docs.djangoproject.com/en/2.1/ref/models/options/#unique-together)
  • 想法#1:使用unique_together - >不适用于M2M字段,如[文档]中所述(https://docs.djangoproject.com/en/2.1/ref/models/options/#unique-together)

  • idea #2: overriding save() method. Does not fully work with M2M, because when calling add or remove method, save() is not called.
  • 想法#2:覆盖save()方法。不能完全使用M2M,因为在调用add或remove方法时,不会调用save()。

  • idea #3: using an explicite through model, but since I'm live in production, I'd like to avoid taking risks on migrating important structures like theses. EDIT: after thinking of it, I don't see how it could help actually.

    想法#3:使用明确的模型,但由于我生活在生产中,我想避免冒险迁移重要的结构,如论文。编辑:在考虑之后,我看不出它实际上是如何帮助的。

  • idea #4: Using a m2m_changedsignal to check the uniqueness anytime the add() method is called.

    想法#4:使用m2m_changedsignal在任何时候调用add()方法时检查唯一性。

I ended up with the idea 4 and thought everything was OK, with this signal...

我最终得到了这个想法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)
                ))

...until today when I found in my database lots of badges with the same identifier but no restaurant (should not happend at the business level) I understood there is no atomicity between the save() and the signal. Which means, if the user have an error about uniqueness when trying to create a badge, the badge is created but without restaurants linked to it.

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

So, the question is: how do you ensure at the model level that if the signal raises an Error, the save() is not commited?

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

Thanks!

3 个解决方案

#1


2  

I see two separate issues here:

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

  1. You want to enforce a particular constraint on your data.

    您希望对数据强制执行特定约束。

  2. If the constraint is violated, you want to revert previous operations. In particular, you want to revert the creation of the Badge instance if any Restaurants are added in the same request that violate the constraint.

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

Regarding 1, your constraint is complicated because it involves multiple tables. That rules out database constraints (well, you could probably do it with a trigger) or simple model-level validation.

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

Your code above is apparently effective at preventing adds that violate the constraint. Note, though, that this constraint could also be violated if the identifier of an existing Badge is changed. Presumably you want to prevent that as well? If so, you need to add similar validation to Badge (e.g. in Badge.clean()).

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

Regarding 2, if you want the creation of the Badge instance to be reverted when the constraint is violated, you need to make sure the operations are wrapped in a database transaction. You haven't told us about the views where these objects area created (custom views? Django admin?) so it's hard to give specific advice. Essentially, you want to have this:

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

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

If you do, an exception thrown by your M2M pre_add signal will rollback the transaction, and you won't get the leftover Badge in your database. Note that admin views are run in a transaction by default, so this should already be happening if you're using the admin.

如果这样做,M2M pre_add信号抛出的异常将回滚事务,并且您将无法在数据库中获得剩余的Badge。请注意,管理员视图默认在事务中运行,因此如果您使用的是管理员,则应该已经发生这种情况。

Another approach is to do the validation before the Badge object is created. See, for example, this answer about using ModelForm validation in the Django admin.

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

#2


0  

I'm afraid the correct way to achieve this really is by adapting the "through" model. But remember that at database level this "through" model already exists, and therefore your migration would simply be adding a unique constraint. It's a rather simple operation, and it doesn't really involve any real migrations, we do it often in production environments.

我担心实现这一目标的正确方法是通过调整“通过”模型。但请记住,在数据库级别,这种“直通”模型已经存在,因此您的迁移只会添加一个唯一约束。这是一个相当简单的操作,并不真正涉及任何真正的迁移,我们经常在生产环境中进行迁移。

Take a look at this example, it pretty much sums everything you need.

看看这个例子,它几乎总结了你需要的一切。

#3


0  

You can specify your own connecting model for your M2M-models, and then add a unique_together constraint in the meta class of the membership model

您可以为M2M模型指定自己的连接模型,然后在成员模型的元类中添加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"),)

This creates an object that's between the Badge and Restaurant which will be unique for each badge per restaurant.

这会在徽章和餐厅之间创建一个对象,每个餐厅的每个徽章都是唯一的。

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

Optional: Save check

You can also add a custom save function where you can manually check for uniqueness. In this way you can manually raise an exception.

您还可以添加自定义保存功能,您可以在其中手动检查唯一性。通过这种方式,您可以手动引发异常。

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)

#1


2  

I see two separate issues here:

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

  1. You want to enforce a particular constraint on your data.

    您希望对数据强制执行特定约束。

  2. If the constraint is violated, you want to revert previous operations. In particular, you want to revert the creation of the Badge instance if any Restaurants are added in the same request that violate the constraint.

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

Regarding 1, your constraint is complicated because it involves multiple tables. That rules out database constraints (well, you could probably do it with a trigger) or simple model-level validation.

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

Your code above is apparently effective at preventing adds that violate the constraint. Note, though, that this constraint could also be violated if the identifier of an existing Badge is changed. Presumably you want to prevent that as well? If so, you need to add similar validation to Badge (e.g. in Badge.clean()).

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

Regarding 2, if you want the creation of the Badge instance to be reverted when the constraint is violated, you need to make sure the operations are wrapped in a database transaction. You haven't told us about the views where these objects area created (custom views? Django admin?) so it's hard to give specific advice. Essentially, you want to have this:

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

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

If you do, an exception thrown by your M2M pre_add signal will rollback the transaction, and you won't get the leftover Badge in your database. Note that admin views are run in a transaction by default, so this should already be happening if you're using the admin.

如果这样做,M2M pre_add信号抛出的异常将回滚事务,并且您将无法在数据库中获得剩余的Badge。请注意,管理员视图默认在事务中运行,因此如果您使用的是管理员,则应该已经发生这种情况。

Another approach is to do the validation before the Badge object is created. See, for example, this answer about using ModelForm validation in the Django admin.

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

#2


0  

I'm afraid the correct way to achieve this really is by adapting the "through" model. But remember that at database level this "through" model already exists, and therefore your migration would simply be adding a unique constraint. It's a rather simple operation, and it doesn't really involve any real migrations, we do it often in production environments.

我担心实现这一目标的正确方法是通过调整“通过”模型。但请记住,在数据库级别,这种“直通”模型已经存在,因此您的迁移只会添加一个唯一约束。这是一个相当简单的操作,并不真正涉及任何真正的迁移,我们经常在生产环境中进行迁移。

Take a look at this example, it pretty much sums everything you need.

看看这个例子,它几乎总结了你需要的一切。

#3


0  

You can specify your own connecting model for your M2M-models, and then add a unique_together constraint in the meta class of the membership model

您可以为M2M模型指定自己的连接模型,然后在成员模型的元类中添加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"),)

This creates an object that's between the Badge and Restaurant which will be unique for each badge per restaurant.

这会在徽章和餐厅之间创建一个对象,每个餐厅的每个徽章都是唯一的。

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

Optional: Save check

You can also add a custom save function where you can manually check for uniqueness. In this way you can manually raise an exception.

您还可以添加自定义保存功能,您可以在其中手动检查唯一性。通过这种方式,您可以手动引发异常。

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)