Le savepoint : un signet nommé au sein d'une transaction
Une transaction classique fonctionne en tout-ou-rien : tout ce qui se trouve entre BEGIN et COMMIT est validé ensemble, ou bien rejeté ensemble. C'est généralement le comportement attendu, mais parfois on a besoin d'une granularité plus fine. Genre : « tente ce lot de modifications ; si ça part en vrille, annule uniquement ce lot et garde le reste de la transaction en vie ».
C'est précisément le rôle d'un savepoint SQLite. Vous posez un signet nommé, vous faites votre travail, puis soit vous conservez les modifications (RELEASE), soit vous revenez au signet (ROLLBACK TO).
Le débit d'Ada et le crédit de Boris sont tous les deux conservés. La mise à jour erronée sur Nobody a été annulée sans perdre le reste de la transaction.
Les trois commandes du SAVEPOINT
L'API tient en trois instructions, pas une de plus :
SAVEPOINT name— pose un repère (un point de sauvegarde).RELEASE SAVEPOINT name— valide tout ce qui a été fait depuis le repère et supprime ce dernier.ROLLBACK TO SAVEPOINT name— annule tout ce qui s'est passé depuis le repère, mais le conserve en place pour que vous puissiez retenter votre coup.
Le mot-clé SAVEPOINT après RELEASE et ROLLBACK TO est facultatif : RELEASE risky et ROLLBACK TO risky fonctionnent tout aussi bien.
La ligne tentative étape 2 n'a jamais existé du point de vue de la base finale. Tout le reste, en revanche, est bien enregistré.
Les savepoints sans transaction explicite
Voici un petit détail intéressant : on peut lancer un SAVEPOINT sans avoir fait de BEGIN au préalable. SQLite ouvre alors discrètement une transaction pour vous, et le savepoint le plus externe joue lui-même le rôle de la transaction. Un RELEASE sur ce savepoint valide les changements, tandis qu'un ROLLBACK TO revient en arrière sans valider.
C'est pour ça qu'on parle parfois des savepoints comme de « transactions nommées ». Mais mélanger les deux styles dans du vrai code, ça devient vite le bazar — il faut choisir. La plupart des devs utilisent un BEGIN ... COMMIT explicite pour la transaction englobante, et réservent les savepoints aux points d'annulation partielle à l'intérieur.
Savepoints imbriqués en SQLite
Les savepoints s'empilent. Vous pouvez en poser un à l'intérieur d'un autre, puis annuler celui du dedans sans toucher à celui du dessus :
Résultat final : a, b, d. Le ROLLBACK TO SAVEPOINT inner a bien supprimé c, mais tout ce qui avait été fait avant inner (l'insertion de b) reste en place, et la transaction continue son cours.
Faire un rollback vers un savepoint extérieur annule aussi tout ce qui a été fait aux niveaux internes — toute la pile au-dessus du nom visé se déroule d'un coup :
b et c ont disparu tous les deux. ROLLBACK TO outer rembobine tout ce qui s'est passé depuis la pose du savepoint outer, y compris inner et l'insertion de c.
Pourquoi utiliser les savepoints SQLite ?
Le cas d'école, c'est le traitement par lots où certains éléments ont le droit d'échouer sans faire tomber tout le lot. On encapsule chaque élément dans un savepoint : en cas d'échec, on revient au savepoint et on passe au suivant.
Dans le code d'une vraie application, l'insertion fautive déclenche une erreur, l'application l'intercepte, lance un ROLLBACK TO, puis poursuit son chemin. Les deux bonnes lignes sont conservées ; la ligne fautive ne contamine pas le lot.
C'est exactement ce schéma que les ORM et les outils de migration utilisent pour implémenter les transactions imbriquées : ils n'imbriquent pas réellement des blocs BEGIN (SQLite ne le permet pas), ils traduisent les appels imbriqués en savepoints.
Quelques pièges à connaître
Voici une poignée de détails qui surprennent souvent ceux qui découvrent les savepoints :
COMMITvalide toujours toute la transaction. Peu importe le nombre de savepoints ouverts —COMMIT(ou son aliasEND) clôt l'ensemble de la transaction externe. Ne voyez pasRELEASEcomme une validation partielle ; rien n'est rendu durable tant que la transaction englobante n'est pas validée.ROLLBACK(sansTO) annule tout. Il met fin à la transaction et abandonne tous les savepoints ouverts. Pour garder la transaction vivante, utilisez plutôtROLLBACK TO nom.- Un savepoint reste ouvert jusqu'à être libéré ou traversé par un rollback. Oublier de faire
RELEASEne fait pas perdre de données, mais le repère reste là jusqu'à la fin de la transaction. - Les noms n'ont pas besoin d'être uniques. Si vous définissez deux fois
SAVEPOINT s, alorsROLLBACK TO scible le plus récent. Pratique pour la récursivité, déroutant si c'est involontaire.
La suite : les vues
Les savepoints offrent un contrôle plus fin sur les écritures. L'étape suivante consiste à façonner la manière dont vous lisez — enregistrer une requête sous forme d'objet nommé et réutilisable, interrogeable avec SELECT comme une table. C'est ce qu'on appelle une vue, et c'est le sujet du prochain chapitre.
Questions fréquentes
Qu'est-ce qu'un savepoint en SQLite ?
Un savepoint, c'est un marqueur nommé que l'on pose à l'intérieur d'une transaction. On peut ensuite faire ROLLBACK TO sur ce nom pour annuler tout ce qui a été fait après, ou RELEASE pour conserver les modifications et retirer le marqueur. En pratique, ça permet de découper une transaction en sous-unités que l'on peut récupérer indépendamment.
Quelle différence entre un savepoint et une transaction en SQLite ?
Une transaction démarre avec BEGIN et se termine par COMMIT ou ROLLBACK. Un savepoint, lui, se déclare à l'intérieur d'une transaction avec SAVEPOINT nom et offre un point d'annulation partiel. Faire un rollback vers un savepoint ne clôt pas la transaction englobante : on peut continuer à travailler et committer plus tard.
Peut-on imbriquer des savepoints en SQLite ?
Oui. On peut empiler plusieurs savepoints avec des noms différents, et ROLLBACK TO outer annule tout jusqu'à ce niveau, y compris les savepoints internes. Les noms n'ont d'ailleurs pas besoin d'être uniques : SQLite utilise le savepoint le plus récent portant ce nom, ce qui correspond au comportement classique d'une pile.