Ад уникальности объектов Django с полями M2M - PullRequest
0 голосов
/ 03 сентября 2018
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, потому что при вызове метода add или remove, save() не вызывается.
  • идея # 3 : использование явной модели through, но, поскольку я живу в производстве, я бы хотел избежать риска при переносе важных структур, таких как тезисы. РЕДАКТИРОВАТЬ : подумав об этом, я не понимаю, как это могло бы помочь на самом деле.

  • idea # 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() не передается?

Спасибо!

Ответы [ 3 ]

0 голосов
/ 24 сентября 2018

Вы можете указать свою собственную модель соединения для своих 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"),)

Это создает объект, который находится между 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)
0 голосов
/ 24 сентября 2018

Боюсь, правильный путь для достижения этой цели - адаптация сквозной модели. Но помните, что на уровне базы данных эта сквозная модель уже существует, и поэтому ваша миграция просто добавит уникальное ограничение. Это довольно простая операция, которая не требует каких-либо реальных миграций, мы часто делаем это в производственных средах.

Взгляните на этот пример , он в значительной степени суммирует все, что вам нужно.

0 голосов
/ 19 сентября 2018

Я вижу два отдельных вопроса здесь:

  1. Вы хотите наложить определенные ограничения на ваши данные.

  2. Если ограничение нарушено, вы хотите отменить предыдущие операции. В частности, вы хотите отменить создание экземпляра Badge, если какие-либо Restaurants добавлены в тот же запрос, который нарушает ограничение.

Что касается 1, ваше ограничение является сложным, поскольку оно включает несколько таблиц. Это исключает ограничения базы данных (возможно, вы могли бы сделать это с помощью триггера) или простой проверки на уровне модели.

Ваш код выше, по-видимому, эффективен для предотвращения adds, который нарушает ограничение. Обратите внимание, однако, что это ограничение также может быть нарушено, если идентификатор существующего Badge изменяется. Предположительно, вы хотите предотвратить это? Если это так, вам нужно добавить аналогичную проверку в Badge (например, в Badge.clean()).

Что касается 2, если вы хотите, чтобы создание экземпляра Badge было отменено при нарушении ограничения, необходимо убедиться, что операции заключены в транзакцию базы данных. Вы не рассказали нам о видах, где создавалась область этих объектов (пользовательские виды? Администратор Django?), Поэтому сложно дать конкретный совет. По сути, вы хотите иметь это:

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

Если вы это сделаете, исключение, выданное вашим сигналом M2M pre_add, откатит транзакцию, и вы не получите остаток Badge в вашей базе данных. Обратите внимание, что представления администратора по умолчанию выполняются в транзакции, поэтому это должно происходить, если вы используете администратора.

Другой подход заключается в проверке перед созданием объекта Badge. См., Например, этот ответ об использовании ModelForm проверки в администраторе Django.

...