• 20 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 12/12/19

Découvrez les transactions

Log in or subscribe for free to enjoy all this course has to offer!

Dans le chapitre précédent vous avez terminé le formulaire de réservation. C'est une grande étape ! Néanmoins, la fonctionnalité "Réservation d'album" n'est pas encore terminée.

En effet, la vue detail contient deux risques possibles d'erreur. Avez-vous deviné lesquels ?

La première erreur vient de la gestion des contacts. S'il n'existe pas, aucun souci : un nouvel item est ajouté dans la base. Mais s'il existe déjà, un bogue important apparaîtra.

Regardez le code actuel :

contact = Contact.objects.filter(email=email)
if not contact.exists():
    # If a contact is not registered, create a new one.
    contact = Contact.objects.create(
        email=email,
        name=name
    )
album = get_object_or_404(Album, id=album_id)
booking = Booking.objects.create(
    contact=contact,
    album=album
)

Avez-vous trouvé l'erreur ? Le contenu de la variable contact n'est pas du même type selon si le contact existe ou pas !Contact.objects.filter(email=email) renvoie un objet de type QuerySet alors que Contact.objects.create() renvoie un objet de type Contact. C'est d'ailleurs ce dernier qu'il faut utiliser pour pouvoir associer le contact à une réservation !

Ajoutez donc un else et associez la variable contact au premier item de la QuerySet :

contact = Contact.objects.filter(email=email)
if not contact.exists():
    # If a contact is not registered, create a new one.
    contact = Contact.objects.create(
        email=email,
        name=name
    )
else:
    contact = contact.first()

Quant au second souci, je vous donne un indice : la vue interagit beaucoup avec la base de données. Elle crée ou récupère un contact, récupère un album, le modifie et ajoute une nouvelle réservation. Que se passe-t-il si l'une de ces opérations échoue ? Une erreur apparaît mais la précédente requête a tout de même été réalisée !

Prenons un exemple. Patrice, un nouveau contact, veut réserver l'album "Highway To Hell". Le système crée son profil puis récupère l'album quand, soudain, une erreur minuscule survient avant de rendre l'album indisponible.

def detail(request, album_id):
    # ...
    contact = Contact.objects.filter(email=email)
    if not contact.exists():
        contact = Contact.objects.create(
            email=email,
            name=name
        )
    else:
        contact = contact.first()

    album = get_object_or_404(Album, id=album_id)
    booking = Booking.objects.create(
        contact=contact,
        album=album
    )

    #########################
    ######### ERROR #########
    #########################
    album.available = False
    album.save()

Une page 500 apparaît. Patrice est mécontent : il doit réserver de nouveau le disque. Etant donné que la page 500 est générique, elle ne donne pas le lien vers l'album. Il le cherche donc de nouveau, effectue sa réservation... quand une nouvelle page d'erreur apparaît. "Screugneugneu, j'aurais mieux fait de commander sur un autre site !" se dit-il, avant de fermer définitivement sa fenêtre.

Savez-vous pourquoi la seconde page d'erreur s'affiche ? Lors de la première tentative de réservation, l'erreur est apparue alors que la réservation avait déjà été créée. Patrice a cru que sa réservation n'avait pas été prise en compte alors qu'en réalité elle l'était ! En voulant recommencer, le système a créé une nouvelle réservation et l'a associée au même album. Cela a levé une erreur car une réservation et un album sont liés par une relation un à un. Autrement dit, un album ne peut être réservé qu'une fois.

Toute personne essayant de commander de nouveau l'album verra une erreur. Plutôt ennuyeux, n'est-ce pas ? Sans parler du fait que le disquaire a perdu un client inutilement.

Comment résoudre ce scénario ? Vous pouvez mettre à jour la base en déclarant l'album comme étant non disponible. Mais cela ne vous prémunit par contre d'autres erreurs du même type.

Django intègre un système extraordinaire pour faire face à cela : les transactions.

Une transaction regroupe plusieurs requêtes à effectuer. Si l'une d'elles échoue, toutes les précédentes sont annulées et les items retournent à leur état original. Si vous aviez isolé vos requêtes dans une transaction, le compte de Patrice et la réservation auraient été supprimés. Il aurait pu recommencer tranquillement.

Il existe deux manières d'activer les transactions dans Django :

  • par défaut : toutes les routes sont, par défaut, dans une transaction.

  • au cas par cas : les routes ne sont pas dans des transactions. Il faut les activer manuellement.

Par défaut, le générateur de Django (manage.py startproject) considère que le développeur doit activer les transactions s'il le souhaite. Voyons comment configurer cela.

Transactions par défaut

Pour activer les transactions par défaut, ajoutez la ligne suivante :

settings.py

DATABASES = {
    'default': {
      # ...
      'ATOMIC_REQUESTS': True,
    }
}

C'est tout ! A présent, toutes les requêtes des vues seront dans des transactions.

Pour qu'une vue ne le soit pas, ajoutez le décorateur @transaction.non_atomic_requests :

views.py

from django.db import transaction

@transaction.non_atomic_requests
def index(request):
    # ...

Transactions au cas par cas

Si vous souhaitez utiliser les transactions ponctuellement, vous n'avez pas à modifier le fichier settings.py. Utilisez la méthode atomic() ou entourez la vue du décorateur @transaction.atomic :

views.py

from django.db import transaction
# ...

@transaction.atomic
def detail(request, album_id):
    contact = Contact.objects.filter(email=email)
    # ...

Ou bien utilisez un bloc with pour spécifier les requêtes qui doivent faire partie de la transaction et celles qui doivent en être exclues :

views.py

from django.db import transaction
# ...

def detail(request, album_id):
    # ...
    with transaction.atomic():
        contact = Contact.objects.filter(email=email)
        # ...

En ajoutant cette ligne, vous venez de résoudre un souci : si une erreur intervient entre plusieurs requêtes, les précédentes seront annulées et la base de données reviendra à son état original.

C'est un grand pas !

Pourtant, l'utilisateur verra toujours une page d'erreur générique. Vous pouvez changer cela en intégrant la transaction dans un bloc try / except. En effet, l'échec d'une transaction renvoie une exception IntegrityError.

views.py

from django.db import transaction, IntegrityError
# ...

def detail(request, album_id):
    # ...
    try:
        with transaction.atomic():
            contact = Contact.objects.filter(email=email)
            # ...
    except IntegrityError:
        pass

Je vous propose d'afficher la page de l'album si la transaction échoue et d'y inclure un message d'erreur. Afin de simplifier le code, il est possible de supprimer le contenu du else et d'ajouter les erreurs en bas de la vue :

views.py

 def detail(request, album_id):
    # ...
    if request.method == 'POST':
        form = ContactForm(request.POST, error_class=ParagraphErrorList)
        if form.is_valid():
            # ...
            try:
                with transaction.atomic():
                    # ...
            except IntegrityError:
                form.errors['internal'] = "Une erreur interne est apparue. Merci de recommencer votre requête."

    else:
        form = ContactForm()

    context['form'] = form
    context['errors'] = form.errors.items()
    return render(request, 'store/detail.html', context)

Testez si tout fonctionne en essayant de réserver deux fois le même album.

Que choisir ?

Activer les transactions par défaut vous garantit de ne jamais les oublier mais peut avoir des conséquences en matière de performance. Afin de bien comprendre le concept, je vous invite à approfondir votre apprentissage en lisant la documentation officielle sur les transactions et le chapitre Transactions du livre Two Scoops of Django.

C'est terminé ! Les réservations sont finies et il est temps d'ajouter l'interface d'administration ! On se retrouve dans le prochain chapitre !

Code de ce chapitre

Retrouvez l'intégralité du code sur ce dépôt GitHub.

Example of certificate of achievement
Example of certificate of achievement