Django — История условий гонки с get_or_create и уникальными ограничениями
Не повторяйтесь. Всегда. Всегда. О, подожди, мой плохой. Извиняюсь.
Здесь я пролил одно из 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
или подобную логику с поиском, который не является «уникальным ограничением» на уровне базы данных!