Django: una historia de condiciones de carrera con get_or_create y restricciones únicas

Dec 01 2022
No te repitas. Alguna vez.

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_togetherrestricción para los campos parece lógico users, document_typeteniendo 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

La ejecución de las funciones antes mencionadas para solicitudes concurrentes

Entonces, asumiendo las restricciones únicas requeridas en su lugar, su implementación arrojará un error de integridad, pero Django get_or_createlo 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_createdevué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_createuna lógica similar con una búsqueda que no tenga "limitaciones únicas" en el nivel de la base de datos!