Django — A Story of Race condições com get_or_create e restrições exclusivas
Não se repita. Sempre. Sempre. Oh, espere, meu mal. Desculpe.
Aqui, eu derramei uma das 101 poções secretas que fazemos todos beberem antes de se juntar ao nosso time! Significa simplesmente que se você está escrevendo algo semelhante duas vezes, deve haver uma maneira melhor de fazer isso.
Recentemente, enquanto escrevíamos nosso serviço de coleta de documentos, sob os efeitos da poção acima. O objetivo era montar um serviço de coleta de documentos mais genérico para coletar todos os tipos de documentos do usuário. Vou falar sobre como nós, nosso código e depois o sentinela entramos em um dilema estranho durante esse tempo e como resolvemos isso.
Uma tabela crítica em qualquer serviço será uma tabela Documento com campos-chave, a saber: user
, document_type (AADHAAR_CARD/PAN_CARD/SALARY_SLIP)
, document. A princípio, adicionar uma unique_together
restrição para os campos users
, e document_type
parece lógico, tendo em mente que um usuário deve possuir apenas um cartão Aadhaar ou PAN. Mas não foi uma das melhores decisões da nossa parte, pois estaremos salvando recibos de salário, declarações de impostos na mesma tabela, que um usuário pode possuir em várias quantidades. Com confiança, colocamos uma validação no método save do nosso Django Model assim e dormimos pacificamente.
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

Portanto, assumindo as restrições exclusivas necessárias em seu lugar, sua implementação lançará um erro de integridade, mas o Django get_or_create
detectará isso e fornecerá uma resposta clara para você.
Mas como não poderíamos colocar nenhum unique_contraint acima, em nosso caso, ele criará alegremente 2 cartões Addhaar para um único usuário (UIDAI, código vermelho! get(user=user, document_type="AADHAAR_CARD")
) get_or_create
retornar-me nunca esperado Múltiplos objetos. FML!
Django 2.2 para o resgate; introduzido UniqueConstraint
, que permite aplicar restrições exclusivas condicionalmente aos seus modelos. No código, algo semelhante a:
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'
)
]
Chegando ao fim, a moral da história é -
Nunca, jamais, use get_or_create
ou lógica semelhante com uma pesquisa que não seja “exclusivamente restrita” no nível do banco de dados!