Transactions de base de données

Django donne quelques moyens de contrôler la gestion des transactions de base de données.

Gestion des transactions de base de données

Comportement de transaction par défaut de Django

Le comportement par défaut de Django est de fonctionner en mode de validation automatique (« autocommit »). Chaque requête est immédiatement validée dans la base de données, sauf si une transaction est en cours. Voir ci-dessous pour plus de détails.

Django utilise automatiquement des transactions ou des points de sauvegarde pour garantir l’intégrité des opérations de l’ORM qui nécessitent plusieurs requêtes, particulièrement les requêtes delete() et update().

La classe TestCase de Django englobe aussi chaque test dans une transaction pour des raisons de performance.

Couplage des transactions aux requêtes HTTP

Une façon fréquente de gérer les transactions sur le Web est d’envelopper chaque requête dans une transaction. Définissez ATOMIC_REQUESTS à True dans la configuration de chaque base de données pour laquelle vous souhaitez activer ce comportement.

Voici comment cela fonctionne. Avant d’appeler une fonction de vue, Django démarre une transaction. Si la réponse est renvoyée sans problème particulier, Django valide la transaction. Si la vue génère une exception, Django annule la transaction.

Il est possible d’effectuer des sous-transactions à l’aide de points de sauvegarde dans le code de la vue, typiquement avec le gestionnaire de contexte atomic(). Cependant, quand la vue se termine, soit tous les changements sont validés, soit tous sont annulés.

Avertissement

Bien que la simplicité de ce modèle de transaction est attrayant, il devient inefficace lorsque le trafic augmente. L’ouverture d’une transaction pour chaque vue présente un certain coût. L’impact sur la performance dépend de la manière dont vos applications font usage des requêtes et de la manière dont la base de données gère les verrous.

Transactions par requête et réponses en flux

Lorsqu’une vue renvoie une réponse en flux (StreamingHttpResponse), la lecture du contenu de la réponse exécute fréquemment du code pour générer le contenu. Comme la vue s’est déjà terminée, ce code s’exécute en dehors de la transaction.

De manière générale, il n’est pas conseillé d’écrire dans la base de données durant la génération d’une réponse en flux, car il n’y a plus de méthode raisonnable pour gérer les erreurs après le début de l’envoi de la réponse.

En pratique, cette fonctionnalité ne fait qu’envelopper chaque fonction de vue dans le décorateur atomic() décrit ci-dessous.

Notez que seule l’exécution de la vue est incluse dans la transaction. Les intergiciels s’exécutent en dehors de la transaction, de même que le rendu des réponses par gabarit.

Lorsque ATOMIC_REQUESTS est actif, il est toujours possible d’empêcher les vues de s’exécuter dans une transaction.

non_atomic_requests(using=None)[source]

Ce décorateur annule l’effet de ATOMIC_REQUESTS pour une vue donnée :

from django.db import transaction

@transaction.non_atomic_requests
def my_view(request):
    do_stuff()

@transaction.non_atomic_requests(using='other')
def my_other_view(request):
    do_stuff_on_the_other_database()

Cela ne fonctionne que lorsque le décorateur est appliqué à la vue elle-même.

Contrôle explicite des transactions

Django fournit une API unifiée pour contrôler les transactions de base de données.

atomic(using=None, savepoint=True)[source]

L’atomicité est la propriété de base des transactions de base de données. atomic permet de créer un bloc de code à l’intérieur duquel l’atomicité est garantie au niveau de la base de données. Si le bloc de code se termine avec succès, les modifications sont validées dans la base de données. Si une exception apparaît, les modifications sont annulées en bloc.

Les blocs atomic peuvent être imbriqués. Dans ce cas, lorsqu’un bloc intérieur se termine avec succès, ses effets peuvent encore être annulés si une exception est générée plus loin dans le bloc englobant.

atomic peut être utilisé comme décorateur:

from django.db import transaction

@transaction.atomic
def viewfunc(request):
    # This code executes inside a transaction.
    do_stuff()

et comme gestionnaire de contexte:

from django.db import transaction

def viewfunc(request):
    # This code executes in autocommit mode (Django's default).
    do_stuff()

    with transaction.atomic():
        # This code executes inside a transaction.
        do_more_stuff()

L’insertion d”atomic dans un bloc try/except est une manière naturelle de gérer les erreurs d’intégrité :

from django.db import IntegrityError, transaction

@transaction.atomic
def viewfunc(request):
    create_parent()

    try:
        with transaction.atomic():
            generate_relationships()
    except IntegrityError:
        handle_exception()

    add_children()

Dans cet exemple, même si generate_relationships() provoque une erreur de base de données en cassant une contrainte d’intégrité, vous pouvez exécuter des requêtes dans add_children() et les modifications de create_parent() sont toujours présentes. Notez que toute opération exécutée dans generate_relationships() aura déjà été annulée proprement lorsque handle_exception() est appelée, ce qui fait que le gestionnaire d’exception peut très bien agir au niveau de la base de données si nécessaire.

Évitez d’intercepter des exceptions à l’intérieur d”atomic!

À la sortie d’un bloc atomic, Django examine si la sortie se fait normalement ou par une exception afin de déterminer s’il doit valider ou annuler la transaction. Si vous interceptez et gérez des exceptions à l’intérieur du bloc atomic, vous cachez à Django le fait qu’un problème est survenu. Cela peut aboutir à des comportements inattendus.

Ce problème concerne plus spécifiquement les exceptions DatabaseError et ses sous-classes telles que IntegrityError. Après une telle erreur, la transaction est cassée et Django procédera à son annulation dès la sortie du bloc atomic. Si vous essayez d’exécuter des requêtes en base de données avant que l’annulation intervienne, Django générera une exception TransactionManagementError. Vous pouvez également rencontrer ce comportement lorsqu’un gestionnaire de signal lié à l’ORM génère une exception.

La manière correcte d’intercepter les erreurs de base de données est de le faire autour du bloc atomic comme dans l’exemple ci-dessus. Si nécessaire, ajoutez un bloc atomic supplémentaire à cet effet. Cette stratégie présente un autre avantage : elle délimite explicitement les opérations qui seront annulées si une exception se produit.

Si vous interceptez les exceptions générées par des requêtes SQL brutes, le comportement de Django n’est pas défini et dépend de la base de données.

Afin de garantir l’atomicité, atomic désactive certaines API. Si vous tentez de valider une transaction, de l’annuler ou de modifier l’état de validation automatique de la connexion de base de données à l’intérieur d’un bloc atomic, vous obtiendrez une exception.

atomic accepte un paramètre using devant correspondre au nom d’une base de données. Si ce paramètre n’est pas présent, Django utilise la base de données "default".

En arrière-plan, le code de gestion des transactions de Django :

  • ouvre une transaction lorsqu’il entre dans le premier bloc atomic;
  • crée un point de sauvegarde lorsqu’il entre dans un bloc atomic imbriqué ;
  • libère le point de sauvegarde ou annule la transaction jusqu’au point de sauvegarde en quittant le bloc imbriqué ;
  • valide ou annule la transaction en quittant le bloc de départ.

Vous pouvez désactiver la création de points de sauvegarde pour les blocs imbriqués en définissant le paramètre savepoint à False. Si une exception survient, Django procède à l’annulation de la transaction au moment de quitter le premier bloc ayant un point de sauvegarde, le cas échéant, ou le bloc initial sinon. L’atomicité est toujours garantie par la transaction du bloc initial. Cette option ne devrait être utilisée que si la création des points de sauvegarde affecte les performances de manière évidente. Le désavantage est que cela casse la gestion d’erreurs telle que décrite précédemment.

Il est possible d’utiliser atomic lorsque la validation automatique est désactivée. Seuls des points de sauvegarde seront utilisés, même pour le bloc initial.

Considérations sur la performance

Les transactions ouvertes constituent une pénalité de performance pour votre serveur de base de données. Pour minimiser cet impact, gardez vos transactions aussi brèves que possible. C’est particulièrement important si vous utilisez atomic() dans des processus de longue durée, en dehors du cycle requête/réponse de Django.

Autocommit

Pourquoi Django utilise-t-il l’autocommit

Dans le standard SQL, chaque requête SQL démarre une transaction, sauf s’il y en a déjà une en cours. De telles transactions doivent ensuite être explicitement soit validées (commit), soit annulées (rollback).

Ce n’est pas toujours très pratique pour les développeurs d’applications. Pour contourner ce problème, la plupart des bases de données mettent à disposition un mode « autocommit ». Lorsque ce mode est actif et qu’il n’y a pas de transaction ouverte, chaque requête SQL est englobée dans sa propre transaction. En d’autres termes, chacune de ces requêtes non seulement démarre une transaction, mais cette transaction est aussi automatiquement validée ou annulée en fonction du résultat de la requête.

PEP 249, la spécification d’API de base de données Python v2.0, exige que le mode autocommit soit initialement désactivé. Django surcharge ce comportement par défaut et active le mode autocommit.

Pour empêcher cela, vous pouvez désactiver la gestion des transactions, mais ce n’est pas recommandé.

Désactivation de la gestion des transaction

Vous pouvez désactiver totalement la gestion des transactions de Django pour une base de données précise en définissant AUTOCOMMIT à False dans sa configuration. Si vous faites cela, Django n’activera pas le mode autocommit et n’effectue aucune opération de commit. Vous obtenez alors le comportement habituel de la bibliothèque de base de données sous-jacente.

Cela demande que vous effectuiez un commit explicite de chaque transaction, même de celles initiées par Django ou par des bibliothèques tierces. Ainsi, cela convient mieux à des situations où vous souhaitez mettre en place votre propre intergiciel de gestion des transactions ou que vous faites des choses plutôt étranges.

Lancement d’actions après le commit

Il peut arriver que vous ayez besoin d’effectuer une action liée à la transaction de base de données en cours, mais seulement si la transaction se termine par un commit réussi. On peut citer comme exemple une tâche Celery, une notification par courriel ou une invalidation de cache.

Django fournit la fonction on_commit() pour inscrire des fonctions de rappel qui seront exécutées après le commit réussi de la transaction :

on_commit(func, using=None)[source]

Passez une fonction (qui n’accepte pas de paramètre) à on_commit():

from django.db import transaction

def do_something():
    pass  # send a mail, invalidate a cache, fire off a Celery task, etc.

transaction.on_commit(do_something)

Il est aussi possible d’envelopper la fonction dans une lambda :

transaction.on_commit(lambda: some_celery_task.delay('arg1'))

La fonction transmise sera appelée immédiatement après une écriture potentielle de base de données pour laquelle on_commit() a été appelée et se terminant par un commit réussi.

Si vous appelez on_commit() alors qu’aucune transaction n’est en cours, la fonction transmise est immédiatement exécutée.

Si cette écriture de base de données potentielle se trouve annulée (rollback), typiquement lorsqu’une exception non traitée est générée dans le bloc atomic(), la fonction est ignorée et ne sera jamais appelée.

Points de sauvegarde (« savepoints »)

Les points de sauvegarde (blocs atomic() imbriqués) sont gérés correctement. C’est-à-dire qu’une fonction inscrite par on_commit() après un point de sauvegarde (dans un bloc atomic() imbriqué) sera appelée après le commit de la transaction la plus externe, mais pas dans le cas où une annulation (rollback) de ce point de sauvegarde ou de tout autre point de sauvegarde précédent se produit pendant la transaction :

with transaction.atomic():  # Outer atomic, start a new transaction
    transaction.on_commit(foo)

    with transaction.atomic():  # Inner atomic block, create a savepoint
        transaction.on_commit(bar)

# foo() and then bar() will be called when leaving the outermost block

D’un autre côté, lorsqu’un point de sauvegarde est annulé (en raison de l’apparition d’une exception), la fonction définie à l’intérieur de point de sauvegarde ne sera pas appelée :

with transaction.atomic():  # Outer atomic, start a new transaction
    transaction.on_commit(foo)

    try:
        with transaction.atomic():  # Inner atomic block, create a savepoint
            transaction.on_commit(bar)
            raise SomeError()  # Raising an exception - abort the savepoint
    except SomeError:
        pass

# foo() will be called, but not bar()

Ordre d’exécution

Les fonctions on_commit d’une transaction donnée sont exécutées dans l’ordre de leur inscription.

Gestion des exceptions

Si l’une des fonctions on_commit dans une transaction donnée génère une exception non traitée, aucune fonction inscrite à la suite dans cette même transaction ne sera exécutée. Ceci correspond évidemment au même comportement qui se serait produit si vous aviez exécuté ces fonctions séquentiellement vous-même sans on_commit().

Ordre d’exécution

Les fonctions de rappel sont exécutées après un commit réussi, ce qui fait qu’une exception dans une fonction de rappel ne provoquera pas d’annulation de transaction. Elles ne sont exécutées qu’en cas de succès de la transaction, mais sans faire partie de la transaction. Pour les cas d’utilisation visés (notifications par courriel, tâches Celery, etc.) cela devrait convenir. Dans le cas contraire (par exemple si l’action à effectuer est si critique que son échec devrait aussi faire échouer la transaction principale), il ne faut alors pas exploiter le point d’entrée on_commit(). Une alternative possible est un commit en deux phases tel que la prise en charge du protocole de commit en deux phases de psycopg ou les extensions facultatives de commit en deux phases dans la spécification DB-API de Python.

Les fonctions de rappel ne sont pas appelées avant que la connexion ne repasse en mode autocommit après le commit (sinon toute requête dans une fonction de rappel ouvrirait une transaction implicite empêchant la connexion de repasser en mode de commit automatique).

Si l’on se trouve déjà en mode autocommit et en-dehors d’un bloc atomic(), la fonction est immédiatement exécutée sans attendre de commit.

Les fonctions ``on_commit« ne fonctionnent qu’en mode autocommit et avec l’API de transaction atomic() (ou ATOMIC_REQUESTS). L’appel à on_commit() lorsque le mode autocommit est désactivé et en-dehors d’un bloc atomique produit une erreur.

Utilisation dans les tests

La classe TestCase de Django enveloppe chaque test dans une transaction et annule cette transaction après chaque test, afin de garantir l’isolation des tests. Cela signifie qu’aucune transaction n’est réellement suivie d’un commit et donc qu’aucune des fonctions de rappel on_commit() ne sera jamais exécutée. Si vous avez besoin de tester les résultats d’une fonction de rappel on_commit(), utilisez plutôt une classe TransactionTestCase.

Pourquoi pas de point d’entrée pour les transactions annulées ?

Un point d’entrée pour les transactions annulées (rollback) est plus difficile à implémenter de manière solide qu’un point d’entrée de commit, car plusieurs choses peuvent provoquer une annulation implicite.

Par exemple, si la connexion à la base de données s’interrompt en raison d’un processus tué sans aucune chance de terminaison propre, le point d’entrée d’annulation ne sera jamais exécuté.

La solution est simple : au lieu de faire quelque chose à l’intérieur du bloc atomique (transaction) puis de le défaire en cas d’échec de transaction, utilisez on_commit() pour déporter la tâche au moment où la transaction a réussi. C’est beaucoup plus facile de défaire quelque chose que l’on n’a jamais fait !

API de bas niveau

Avertissement

Si possible, préférez toujours atomic(). Il prend en compte les particularités de chaque base de données et évite les opérations non valides.

L’API de bas niveau n’est utile que si vous implémentez votre propre gestion des transactions.

Autocommit

Django fournit une API basique dans le module django.db.transaction pour gérer l’état de validation automatique de chaque connexion de base de données.

get_autocommit(using=None)[source]
set_autocommit(autocommit, using=None)[source]

Ces fonctions acceptent un paramètre using qui doit correspondre au nom d’une base de données. Si ce paramètre n’est pas présent, Django utilise la base de données "default".

La validation automatique (« autocommit ») est initialement activée. Si vous la désactivez, il est de votre responsabilité de la restaurer ensuite.

Dès que vous désactivez la validation automatique, vous obtenez le comportement par défaut de votre adaptateur de base de données et Django ne vous aide plus. Même si ce comportement fait l’objet de la PEP 249, les implémentations des adaptateurs ne sont pas toujours cohérentes entre elles. Parcourez attentivement la documentation de l’adaptateur que vous utilisez.

Vous devez vous assurer qu’aucune transaction n’est pendante, généralement en appelant commit() ou rollback() avant de réactiver la validation automatique.

Django refuse de désactiver la validation automatique lorsqu’un bloc atomic() est actif, car l’atomicité ne serait alors plus respectée.

Transactions

Une transaction est un ensemble atomique de requêtes de base de données. Même si votre programme se plante, la base de données garantit que soit tous les changements seront appliqués, soit aucun.

Django n’offre pas d’API pour démarrer une transaction. La manière attendue de démarrer une transaction est de désactiver la validation automatique avec set_autocommit().

Une fois dans la transaction, vous pouvez choisir d’appliquer les modifications effectuées jusqu’à ce point avec commit(), ou de toutes les annuler avec rollback(). Ces fonctions sont définies dans django.db.transaction.

commit(using=None)[source]
rollback(using=None)[source]

Ces fonctions acceptent un paramètre using qui doit correspondre au nom d’une base de données. Si ce paramètre n’est pas présent, Django utilise la base de données "default".

Django refuse de valider ou d’annuler une transaction lorsqu’un bloc atomic() est actif, car l’atomicité ne serait alors plus respectée.

Points de sauvegarde (« savepoints »)

Un point de sauvegarde est un marqueur dans une transaction qui vous permet d’annuler une transaction en partie, plutôt que dans sa totalité. Les points de sauvegarde sont disponibles pour les moteurs SQLite (≥ 3.6.8), PostgreSQL, Oracle et MySQL (avec le moteur de stockage InnoDB). D’autres moteurs fournissent les fonctions des points de sauvegarde, mais ces fonctions sont vides, elles ne font rien du tout.

Les points de sauvegarde ne sont pas particulièrement utiles quand la validation automatique est active, ce qui est le comportement par défaut de Django. Cependant, dès que vous ouvrez une transaction avec atomic(), vous accumulez une série d’opérations de base de données en attente de validation ou d’annulation. Lorsque vous annulez avec un « rollback », toute la transaction est annulée Les points de sauvegarde permettent d’annuler des opérations de manière plus sélective, plutôt que d’annuler en bloc comme le fait transaction.rollback().

Lorsque le décorateur atomic() est imbriqué, il crée un point de sauvegarde pour permettre une validation ou une annulation partielle. Vous êtes fortement encouragé à utiliser atomic() plutôt que les fonctions présentées ci-dessous, mais elles font tout de même partie de l’API publique, et il n’est pas prévu de les rendre obsolètes.

Chacune de ces fonctions accepte un paramètre using devant correspondre au nom de la base de données pour laquelle le comportement s’applique. Si aucun paramètre using n’est transmis, c’est la base de données "default" qui est utilisée.

Les points de sauvegarde sont contrôlés par trois fonctions dans django.db.transaction:

savepoint(using=None)[source]

Crée un nouveau point de sauvegarde. Un point est marqué dans la transaction, à un état qui est reconnu comme « bon ». Renvoie l’identifiant du point de sauvegarde (sid).

savepoint_commit(sid, using=None)[source]

Libère le point de sauvegarde sid. Les modifications effectuées depuis la création du point de sauvegarde sont intégrées dans la transaction.

savepoint_rollback(sid, using=None)[source]

Annule la transaction en revenant au point de sauvegarde sid.

Ces fonctions ne font rien si les points de sauvegarde ne sont pas pris en charge ou si la base de données est en mode de validation automatique.

Une fonction utilitaire est également disponible :

clean_savepoints(using=None)[source]

Réinitialise le compteur utilisé pour générer les identifiants uniques des points de sauvegarde.

L’exemple suivant illustre l’utilisation des points de sauvegarde :

from django.db import transaction

# open a transaction
@transaction.atomic
def viewfunc(request):

    a.save()
    # transaction now contains a.save()

    sid = transaction.savepoint()

    b.save()
    # transaction now contains a.save() and b.save()

    if want_to_keep_b:
        transaction.savepoint_commit(sid)
        # open transaction still contains a.save() and b.save()
    else:
        transaction.savepoint_rollback(sid)
        # open transaction now contains only a.save()

Les points de sauvegarde peuvent être utilisés pour se rétablir après une erreur de base de données en effectuant une annulation partielle des opérations. Si vous faites cela à l’intérieur d’un bloc atomic(), tout le bloc sera quand même annulé, car Django ne sait pas que vous avez géré la situation à un plus bas niveau ! Pour empêcher cela, vous pouvez contrôler le comportement d’annulation avec les fonctions suivantes.

get_rollback(using=None)[source]
set_rollback(rollback, using=None)[source]

En définissant le drapeau rollback à True, vous forcez une annulation lorsque vous sortez du bloc atomic le plus proche. Cela peut être utile pour provoquer une annulation sans générer d’exception.

En le définissant à False, vous empêchez une telle annulation. Avant de faire cela, assurez-vous d’avoir bien annulé la transaction jusqu’à un point de sauvegarde en bon état à l’intérieur du bloc atomic actuel. Sinon, vous cassez l’atomicité et des corruptions de données peuvent apparaître.

Notes spécifiques à certaines bases de données

Points de sauvegarde dans

Même si les points de sauvegarde sont pris à charge à partir de SQLite ≥ 3.6.8, un défaut de conception dans le module sqlite3 les rend presque inutilisables.

Lorsque la validation automatique est active, les points de sauvegarde n’ont pas de raison d’être. Dans le cas contraire, sqlite3 valide implicitement la transaction avant les instructions de points de sauvegarde (en fait, il valide avant toute instruction autre que SELECT, INSERT, UPDATE, DELETE et REPLACE). Ce bogue a deux conséquences :

  • L’API de bas niveau des points de sauvegarde n’est utilisable qu’à l’intérieur d’une transaction, c’est-à-dire dans un bloc atomic().
  • Il est impossible d’utiliser atomic() lorsque la validation automatique est désactivée.

Transactions dans MySQL

Si vous utilisez MySQL, la prise en charge des transactions par vos tables varie ; tout dépend de la version de MySQL et des types de tables que vous utilisez (par « type de table », nous entendons quelque chose comme « InnoDB » ou « MyISAM »). Les particularités des transactions de MySQL vont au-delà du thème de cette documentation, mais le site de MySQL possède des informations sur les transactions dans MySQL.

Si votre configuration MySQL ne gère pas les transactions, Django fonctionne toujours en mode de validation automatique : les instructions sont exécutées et validées dès qu’elles sont émises. Si votre configuration MySQL gère les transactions, Django traite les transactions comme expliqué dans ce document.

Traitement des exceptions dans les transactions PostgreSQL

Note

Cette section n’a de sens que si vous implémentez votre propre gestion des transactions. Ce problème ne peut pas survenir dans le mode par défaut de Django et atomic() s’en charge automatiquement.

À l’intérieur d’une transaction, lorsque l’appel à un curseur PostgreSQL génère une exception (typiquement IntegrityError), toutes les commandes SQL suivantes dans la même transaction échouent avec l’erreur « current transaction is aborted, queries ignored until end of transaction block ». Bien qu’il soit improbable qu’une utilisation simple de save() génère une exception avec PostgreSQL, il y a des schémas d’utilisation plus pointus qui sont susceptibles de le faire, comme l’enregistrement d’objets avec des champs uniques, l’enregistrement avec les options force_insert/force_update ou l’appel à des instructions SQL personnalisées.

Il existe plusieurs manières de se sortir de ce genre d’erreurs.

Annulation de la transaction

La première option est d’annuler la totalité de la transaction. Par exemple :

a.save() # Succeeds, but may be undone by transaction rollback
try:
    b.save() # Could throw exception
except IntegrityError:
    transaction.rollback()
c.save() # Succeeds, but a.save() may have been undone

L’appel de transaction.rollback() annule la totalité de la transaction. Toute opération de base de données non validée sera perdue. Dans cet exemple, les modifications effectuées par a.save() seront perdues, même si cette opération n’a pas elle-même généré d’erreur.

Annulation du point de sauvegarde

Vous pouvez utiliser des points de sauvegarde pour contrôler l’étendue d’une annulation. Avant d’effectuer une opération de base de données potentiellement délicate, vous pouvez définir ou mettre à jour le point de sauvegarde ; de cette façon, si l’opération échoue, vous pouvez annuler précisément l’opération concernée, plutôt que la totalité de la transaction. Par exemple :

a.save() # Succeeds, and never undone by savepoint rollback
sid = transaction.savepoint()
try:
    b.save() # Could throw exception
    transaction.savepoint_commit(sid)
except IntegrityError:
    transaction.savepoint_rollback(sid)
c.save() # Succeeds, and a.save() is never undone

Dans cet exemple, a.save() ne sera pas annulé dans le cas où b.save() génère une exception.

Back to Top