Django: una historia de condiciones de carrera con get_or_create y restricciones únicas
No te repitas. Alguna vez. Alguna vez. Oh, espera, mi error. Lo siento.
¡Aquí, derramé una de las 101 pociones secretas que hacemos que todos beban antes de unirse a nuestro equipo! Simplemente significa que si está escribiendo algo similar dos veces, debe haber una mejor manera de hacerlo.
Recientemente, mientras escribíamos nuestro servicio de recopilación de documentos, bajo los efectos de la poción anterior. El objetivo era montar un servicio de recogida de documentos más genérico para recoger todo tipo de documentos de los usuarios. Hablaré sobre cómo nosotros, nuestro código y luego el centinela nos metimos en un extraño dilema durante este tiempo y cómo lo solucionamos.
Una tabla crítica en cualquier servicio de este tipo será una tabla Documento con campos clave, a saber: user
, document_type (AADHAAR_CARD/PAN_CARD/SALARY_SLIP)
, documento. Al principio, agregar una unique_together
restricción para los campos parece lógico users
, document_type
teniendo en cuenta que un usuario debe poseer solo una tarjeta Aadhaar o PAN. Pero no fue una de las mejores decisiones de nuestra parte dado que guardaremos recibos de sueldo, declaraciones de impuestos en la misma tabla, que un usuario puede poseer en múltiples cantidades. Con confianza, pusimos una validación en el método de guardado de nuestro modelo Django de esta manera y nos fuimos a dormir en paz.
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

Entonces, asumiendo las restricciones únicas requeridas en su lugar, su implementación arrojará un error de integridad, pero Django get_or_create
lo detectará y le proporcionará una respuesta ordenada.
Pero como no pudimos poner ninguna contradicción única arriba, en nuestro caso, felizmente creará 2 tarjetas Addhaar para un solo usuario ( get(user=user, document_type="AADHAAR_CARD")
¡ UIDAI, código rojo!) get_or_create
devuélveme nunca esperé Múltiples objetos. FML!
Django 2.2 al rescate; introducido UniqueConstraint
, que le permite aplicar condicionalmente restricciones únicas a sus modelos. En código, algo similar 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'
)
]
Llegando a su fin, la moraleja de la historia es:
¡ Nunca use get_or_create
una lógica similar con una búsqueda que no tenga "limitaciones únicas" en el nivel de la base de datos!