Django — История условий гонки с get_or_create и уникальными ограничениями

Dec 01 2022
Не повторяйтесь. Всегда.

Не повторяйтесь. Всегда. Всегда. О, подожди, мой плохой. Извиняюсь.

Здесь я пролил одно из 101 секретного зелья, которое мы заставляем всех пить перед тем, как присоединиться к нашей команде! Это просто означает, что если вы пишете что-то похожее дважды, должен быть лучший способ сделать это.

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

Критической таблицей в любом таком сервисе будет таблица Document с ключевыми полями, а именно: user, document_type (AADHAAR_CARD/PAN_CARD/SALARY_SLIP), document. Во-первых, добавление unique_togetherограничения для полей кажется логичным users, document_typeучитывая, что у одного пользователя должна быть только одна карта Aadhaar или PAN. Но это было не одно из лучших решений с нашей стороны, учитывая, что мы будем сохранять зарплатные ведомости и налоговые декларации в одной таблице, которой один пользователь может владеть в нескольких количествах. Мы уверенно проверили метод сохранения нашей модели Django и продолжили спать спокойно.

def save(self):
  if not self.document_type not in ['AADHAAR_CARD', 'PAN_CARD']:
    if Document.objects.filter(
          user=self.user, 
          document_type=self.document_type
       ).exclude(id=self.id).exists():
      raise ValidationError("Not allowed")
  super().save()

user_aadhaar_card = None
try:
  user_aadhaar_card = Document.objects.get(
        user=request.user, 
        document_type="AADHAAR_CARD"
)
except UserDocumentMapping.DoesNotExist:
  user_aadhaar_card = Document.objects.create(
        user=request.user, 
        document_type=AADHAAR_CARD
)

try:
  return some_model.get(**queryset), False
except some_model.DoesNotExist:
  try:
    instance = some_model.create(**queryset)
    return instance, True
  except IntegrityError:
    return some_model.get(**queryset), False

Выполнение вышеупомянутых функций для параллельных запросов

Таким образом, предполагая наличие требуемых уникальных ограничений вместо них, ваша реализация выдаст ошибку целостности, но Django get_or_createобнаружит ее и предоставит вам четкий ответ.

Но поскольку мы не могли указать выше уникальное ограничение, в нашем случае он с радостью создаст 2 карты Addhaar для одного пользователя (UIDAI, красный код get(user=user, document_type="AADHAAR_CARD")! get_or_create) вернуть меня никогда не ожидал несколько объектов. ФМЛ!

Django 2.2 спешит на помощь; представил UniqueConstraint, что позволяет условно применять уникальные ограничения к вашим моделям. В коде что-то похожее на:

class Document(models.Model)
  user = <>
  document_type = <>
  document = <>

  class Meta:
      constraints = [
        models.UniqueConstraint(
          fields=['user', 'document_type'], 
          condition=Q(
            document_type__in=["AADHAAR_CARD", "PAN_CARD"]
          ),
          name='unique_user_unique_document_type'
        )
      ]

Подходит к концу, мораль этой истории такова:

Никогда не используйте get_or_createили подобную логику с поиском, который не является «уникальным ограничением» на уровне базы данных!