Django — Opowieść o warunkach wyścigu z ograniczeniami get_or_create i unique
Nie powtarzaj się. Kiedykolwiek. Kiedykolwiek. Och, czekaj, mój błąd. Przepraszam.
Tutaj wylałem jeden ze 101 sekretnych eliksirów, które każdy pije przed dołączeniem do naszego zespołu! Oznacza to po prostu, że jeśli piszesz coś podobnego dwa razy, musi istnieć lepszy sposób, aby to zrobić.
Niedawno, gdy pisaliśmy naszą usługę zbierania dokumentów, pod wpływem powyższej mikstury. Celem było stworzenie bardziej ogólnej usługi gromadzenia dokumentów w celu gromadzenia wszelkiego rodzaju dokumentów użytkowników. Opowiem o tym, jak my, nasz kod, a potem wartownik wpadliśmy w dziwny dylemat w tym czasie i jak go rozwiązaliśmy.
Krytyczną tabelą w każdej takiej usłudze będzie tablica dokumentów z kluczowymi polami, a mianowicie: user
, document_type (AADHAAR_CARD/PAN_CARD/SALARY_SLIP)
, dokument. Na początku dodanie unique_together
ograniczenia dla pól users
i document_type
wydaje się logiczne, pamiętając, że jeden użytkownik powinien posiadać tylko jedną kartę Aadhaar lub PAN. Ale nie była to jedna z najlepszych decyzji z naszej strony, ponieważ będziemy zapisywać odcinki wynagrodzeń, deklaracje podatkowe w tej samej tabeli, którą jeden użytkownik może posiadać w wielu ilościach. Pewno dokonaliśmy walidacji metody zapisu naszego modelu Django w ten sposób i poszliśmy spać spokojnie.
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

Tak więc, zakładając wymagane unikalne ograniczenia w ich miejsce, twoja implementacja zgłosi błąd integralności, ale Django get_or_create
go złapie i zapewni ci zgrabną odpowiedź.
Ale ponieważ nie mogliśmy umieścić żadnego unique_contraint powyżej, w naszym przypadku z radością utworzy 2 karty Addhaar dla jednego użytkownika (UIDAI, kod czerwony get(user=user, document_type="AADHAAR_CARD")
! get_or_create
) zwróć mi nigdy się nie spodziewałem Wiele obiektów. FML!
Django 2.2 na ratunek; wprowadzono UniqueConstraint
, co pozwala warunkowo zastosować unikatowe ograniczenia do modeli. W kodzie coś podobnego do:
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'
)
]
Morał tej historii jest taki, że —
Nigdy nie używaj get_or_create
ani podobnej logiki z wyszukiwaniem, które nie jest „ograniczone przez unikalność” na poziomie bazy danych!