Django — A Story of Race conditions avec get_or_create et contraintes uniques

Dec 01 2022
Ne vous répétez pas. Déjà.

Ne vous répétez pas. Déjà. Déjà. Oh, attendez, mon mauvais. Pardon.

Ici, j'ai renversé l'une des 101 potions secrètes que nous faisons boire à tout le monde avant de rejoindre notre équipe ! Cela signifie simplement que si vous écrivez quelque chose de similaire deux fois, il doit y avoir une meilleure façon de le faire.

Récemment, alors que nous écrivions notre service de collecte de documents, sous les effets de la potion ci-dessus. L'objectif était de monter un service de collecte de documents plus générique pour collecter toutes sortes de documents d'utilisateurs. Je vais parler de la façon dont nous, notre code, puis la sentinelle nous sommes retrouvés dans un dilemme étrange pendant cette période et comment nous l'avons résolu.

Une table critique dans un tel service sera une table Document avec des champs clés, à savoir : user, document_type (AADHAAR_CARD/PAN_CARD/SALARY_SLIP), document. Au début, ajouter une unique_togethercontrainte pour les champs users, et document_typesemble logique, en gardant à l'esprit qu'un utilisateur ne doit posséder qu'une seule carte Aadhaar ou PAN. Mais ce n'était pas l'une des meilleures décisions de notre part étant donné que nous allons enregistrer les bulletins de salaire, les déclarations de revenus dans le même tableau, qu'un utilisateur peut posséder en plusieurs quantités. En toute confiance, nous avons mis une validation sur la méthode de sauvegarde de notre modèle Django comme celui-ci et avons continué à dormir paisiblement.

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

L'exécution des fonctions susmentionnées pour les demandes simultanées

Ainsi, en supposant les contraintes uniques requises à leur place, votre implémentation générera une erreur d'intégrité, mais Django l' get_or_createattrapera et vous fournira une réponse soignée.

Mais comme nous ne pouvions pas mettre de unique_contraint ci-dessus, dans notre cas, il créera avec plaisir 2 cartes Addhaar pour un seul utilisateur (UIDAI, code rouge !) et la prochaine fois que je lancerai en toute confiance en m'appuyant get(user=user, document_type="AADHAAR_CARD")sur ma mauvaise fonction de nettoyage et sur get_or_createDjango retournez-moi jamais attendu plusieurs objets. FML !

Django 2.2 à la rescousse ; introduit UniqueConstraint, qui vous permet d'appliquer conditionnellement des contraintes uniques à vos modèles. Dans le code, quelque chose de similaire à :

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'
        )
      ]

À la fin, la morale de l'histoire est de -

N'utilisez jamais get_or_createune logique similaire avec une recherche qui n'est pas "contrainte unique" au niveau de la base de données !