django-rest-framework Le modèle Django empêche les mises à jour simultanées des données avec un contrôle exclusif optimiste de PostgreSQL

introduction

Le modèle Django n'a pas de mécanisme de contrôle exclusif optimiste. (Aucun droit?) Cette section décrit un exemple d'implémentation pour un contrôle exclusif optimiste dans PostgreSQL.

Remarque)

Création d'un modèle de contrôle d'accès concurrentiel

Obtenir la colonne xmin

Créez une sous-classe qui étend l'expression de requête pour obtenir les colonnes système PostgreSQL.

from django.db.models import Expression, PositiveIntegerField


class XMin(Expression):
    output_field = PositiveIntegerField()

    def as_postgresql(self, compiler, connection):
        return f'"{compiler.query.base_table}"."xmin"', ()

Création d'une classe de gestionnaire de contrôle d'accès concurrentiel

Remplacez get_queryset et ajoutez une colonne de version de ligne (row_version) avec ʻannotate`.

from django.db.models import Manager


class ConcurrentManager(Manager):
    def get_queryset(self):
        super_query = super().get_queryset().annotate(row_version=XMin())
        return super_query

Traitement des exceptions au moment de l'exécution simultanée

Si une erreur se produit pendant le contrôle d'accès concurrentiel, l'exception personnalisée suivante sera émise.

class DbUpdateConcurrencyException(Exception):
    pass

Création d'un modèle de contrôle d'accès concurrentiel

Modifiez la classe du gestionnaire de contrôle d'accès concurrentiel pour remplacer la méthode save et implémenter le contrôle d'accès concurrentiel. Si la ligne à mettre à jour n'est pas trouvée, émettez une exception DbUpdateConcurrencyException.

from django.db.models import Model


class ConcurrentModel(Model):
    objects = ConcurrentManager()

    class Meta:
        abstract = True

        base_manager_name = 'objects'

    def save(self, **kwargs):
        cls = self.__class__
        if self.pk and not kwargs.get('force_insert', None):
            rows = cls.objects.filter(
                pk=self.pk, row_version=self.row_version)
            if not rows:
                raise DbUpdateConcurrencyException(cls.__name__, self.pk)

        super().save(**kwargs)

Changement de modèle

Modifiez la source d'héritage de «Model» à «ConcurrentModel».

class Customer(ConcurrentModel):
    id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    code = models.CharField(verbose_name='code', help_text='code', max_length=10)
    name = models.CharField(verbose_name='Nom', help_text='Nom', max_length=50)

Changement de sérialiseur

Ajoutez une version de ligne (row_version).

class CustomerSerializer(DynamicFieldsModelSerializer):
    row_version = serializers.IntegerField()

    class Meta:
        model = Customer

        fields = (
            'id',
            'code',
            'name',
            'row_version',
        )

Contrôle de fonctionnement du contrôle d'accès concurrentiel

Obtenez des données

Vous pouvez confirmer que la version de ligne a été obtenue.

curl -s -X GET "http://localhost:18000/api/customers/" -H "accept: application/json" | jq .
[
  {
    "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx1",
    "code": "001",
    "name": "test1",
    "row_version": 588
  },
  {
    "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx2",
    "code": "002",
    "name": "test2",
    "row_version": 592
  }
]

Acquisition de données de la première ligne

curl -s -X GET "http://localhost:18000/api/customers/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx1/" -H "accept: application/json" | jq .
{
  "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx1",
  "code": "001",
  "name": "test",
  "row_version": 588
}

Si vous donnez une version de ligne différente

500 est renvoyé car une erreur se produit en raison du contrôle d'accès concurrentiel.

curl -X PUT "http://localhost:18000/api/customers/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx1/" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"code\": \"001\", \"name\": \"test2\", \"row_version\": 0}"

<!doctype html>
<html lang="en">
<head>
  <title>Server Error (500)</title>
</head>
<body>
  <h1>Server Error (500)</h1><p></p>
</body>
</html>

Étant donné la même version de ligne

J'ai pu m'inscrire avec succès.

curl -X PUT "http://localhost:18000/api/customers/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx1/" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"code\": \"001\", \"name\": \"test2\", \"row_version\": 588}" | jq .

{
  "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx1",
  "code": "001",
  "name": "test2",
  "row_version": 588
}

La gestion des erreurs

Si rien n'est fait, il reviendra avec une erreur 500, alors contrôlez-le pour qu'il revienne avec 400. django-rest-framework a la capacité de personnaliser la gestion des exceptions.

Utilisez cette option pour contrôler la réponse aux erreurs de contrôle d'accès concurrentiel qui se produisent dans la vue API.

api/handlers.custom_exception_handler


from rest_framework import status
from rest_framework.validators import ValidationError
from rest_framework.response import Response
from rest_framework.views import exception_handler
from xxxxx import DbUpdateConcurrencyException


def custom_exception_handler(exc, context):
    response = exception_handler(exc, context)

    if isinstance(exc, DbUpdateConcurrencyException):
        return Response(ValidationError({'db_update_concurrency': ['Il a été modifié par un autre utilisateur.']}).detail, status=status.HTTP_400_BAD_REQUEST)

    return response

app/settings.py


REST_FRAMEWORK = {
  'EXCEPTION_HANDLER': 'api.handlers.custom_exception_handler',
}

Contrôle de fonctionnement 2

Si vous donnez une version de ligne différente

Il sera retourné à 400 sous la forme suivante.

curl -X PUT "http://localhost:18000/api/customers/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx1/" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"code\": \"001\", \"name\": \"test2\", \"row_version\": 0}"

{
  "db_update_concurrency": [
    "Il a été modifié par un autre utilisateur."
  ]
}

Recommended Posts

django-rest-framework Le modèle Django empêche les mises à jour simultanées des données avec un contrôle exclusif optimiste de PostgreSQL
Utilisez Django pour enregistrer les données de tweet
Jointure externe gauche dans le modèle Django
Générer automatiquement un diagramme de relation de modèle avec Django