Aller au contenu
English

Transactions

  • Leçon 7
  • 50 min
  • Module 2 : Concevoir un schéma relationnel

Ce que vous saurez faire

Faites en sorte qu’un changement touchant deux lignes se réalise complètement ou pas du tout. Ensuite, empêchez deux requêtes simultanées d’effacer leurs changements respectifs, sans erreur visible pour quiconque.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 50 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • Le projet après la dernière leçon, avec les migrations 005 et 006 appliquées. python -m pytest affiche 49 passed (49 tests réussis).
  • PostgreSQL en marche, avec le petit jeu de données chargé. En cas de doute, lancez python -m scripts.load --reset.

Cette leçon ajoute scripts/lost_update.py et tests/test_transactions.py. Votre projet correspond ensuite à l’instantané end-of-m02.

Vous transférez 50 dollars de votre compte d’épargne à votre compte courant. Pour la banque, cela représente deux changements : retirer 50 du compte d’épargne et ajouter 50 au compte courant. Si l’ordinateur de la banque s’arrête entre les deux, 50 dollars disparaissent. Personne n’accepterait « presque tout le virement ». La banque effectue les deux changements, ou aucun.

Le service de soutien effectue le même type de changement. Quand un client écrit dans le billet de soutien T-30002, deux lignes changent. Il faut une nouvelle ligne dans messages et message_count + 1 dans le billet. Depuis la dernière leçon, le compte est une copie, et cette copie doit rester juste.

Essayez dans psql. Insérez un message, puis n’exécutez pas le UPDATE (mise à jour). Cela représente un programme qui plante ou perd sa connexion après la première instruction :

INSERT INTO messages (ticket_id, author, body)
VALUES ('T-30002', 'agent', 'We have refunded the second charge.');
SELECT message_count,
(SELECT count(*) FROM messages m WHERE m.ticket_id = t.ticket_id) AS messages
FROM tickets t WHERE ticket_id = 'T-30002';
INSERT 0 1
message_count | messages
---------------+----------
1 | 2
(1 row)

Le billet indique 1 message, et la table en contient 2. La liste des billets affichera le mauvais nombre à chaque agent, sans erreur pour les avertir. Cette panne est simulée : vous vous êtes arrêté vous-même. Mais les programmes s’arrêtent parfois entre deux instructions, par exemple quand un déploiement les redémarre.

Cette requête trouve tous les billets dont la copie est incorrecte. Elle renvoie seulement T-30002 :

SELECT ticket_id, message_count FROM tickets t
WHERE message_count <> (SELECT count(*) FROM messages m WHERE m.ticket_id = t.ticket_id);

Corrigez la copie maintenant pour que les prochains exemples commencent avec un compte juste :

UPDATE tickets t
SET message_count = (SELECT count(*) FROM messages m WHERE m.ticket_id = t.ticket_id)
WHERE ticket_id = 'T-30002';

Une transaction est un groupe d’instructions que la base de données traite comme un seul changement. Vous la commencez avec BEGIN (commencer). COMMIT (valider) enregistre le tout. ROLLBACK (annuler) annule le tout, et une erreur dans une transaction entraîne aussi une annulation. Avant la validation, les autres sessions ne voient aucun de ces changements.

Voici le même changement dans une transaction, avec une erreur dans la troisième instruction. Un programme envoie une réponse automatique avec l’auteur robot, que la règle messages_author_known refuse :

BEGIN;
INSERT INTO messages (ticket_id, author, body) VALUES ('T-30002', 'customer', 'Thank you!');
UPDATE tickets SET message_count = message_count + 1, last_message_at = now()
WHERE ticket_id = 'T-30002';
INSERT INTO messages (ticket_id, author, body) VALUES ('T-30002', 'robot', 'Auto-reply: we got your message.');
SELECT count(*) FROM messages WHERE ticket_id = 'T-30002';
COMMIT;
BEGIN
INSERT 0 1
UPDATE 1
ERROR: new row for relation "messages" violates check constraint "messages_author_known"
DETAIL: Failing row contains (334, T-30002, robot, Auto-reply: we got your message., 2026-10-10 04:46:03.681432+00).
ERROR: current transaction is aborted, commands ignored until end of transaction block
ROLLBACK

Après l’erreur, PostgreSQL ignore chaque instruction jusqu’à la fin de la transaction, et le COMMIT devient un ROLLBACK. (Votre numéro de message et votre heure diffèrent.) Vérifiez le billet : message_count reste égal au nombre de messages. Le premier INSERT (insertion) et le UPDATE ont fonctionné, puis les deux ont été annulés. Exécutez la transaction de nouveau sans la ligne du robot. Elle se termine par COMMIT : les deux nombres augmentent maintenant de un ensemble.

Une requête pour ajouter un message à T-30002 entre dans un cadre pointillé représentant une transaction : tout ou rien. À l'intérieur : BEGIN, puis INSERT INTO messages, puis UPDATE tickets SET message_count = message_count + 1. Si les deux réussissent, COMMIT : le nouveau message et message_count + 1 sont enregistrés ensemble. Si une étape échoue, ROLLBACK : aucun changement, aucun message et le même message_count. Une note indique que les autres sessions ne voient rien avant COMMIT.
La limite de la transaction « ajouter un message » : l'insertion et le changement du compte sont enregistrés ensemble, ou pas du tout.
Description textuelle du diagramme

Une requête pour ajouter un message à T-30002 entre dans une transaction. À l’intérieur se trouvent BEGIN, l’instruction INSERT dans messages et l’instruction UPDATE de message_count dans tickets. Si les deux instructions réussissent, COMMIT enregistre le nouveau message et le nouveau compte ensemble. Si une étape échoue, ROLLBACK ne laisse rien : aucun message et le même compte. Les autres sessions ne voient aucun changement avant le COMMIT.

Les spécialistes des bases de données résument les garanties d’une transaction avec quatre lettres, ACID :

  • Atomique : tout ou rien, comme vous venez de le voir. « Atome » vient du grec et signifie « qui ne peut pas être coupé ».
  • Cohérente : chaque règle de la dernière leçon tient quand la transaction est validée.
  • Isolée : les sessions simultanées ne voient pas les changements inachevés des autres.
  • Durable : après COMMIT, le changement survit à une panne. PostgreSQL l’écrit dans un journal sur disque avant d’annoncer COMMIT.

Vous pouvez facilement observer l’isolation avec deux terminaux. Dans chacun, ouvrez psql. Dans le premier (A), exécutez BEGIN; et UPDATE tickets SET message_count = 5 WHERE ticket_id = 'T-30002';. Dans le second (B), lisez le compte avec SELECT message_count FROM tickets WHERE ticket_id = 'T-30002';. B voit l’ancienne valeur, pas 5. Exécutez ROLLBACK; dans A, et le 5 n’aura jamais existé pour les autres.

Quand vous avez terminé, remettez les données dans leur état initial :

Fenêtre de terminal
python -m scripts.load --reset

Amira demande : « Comment je fais ça en Python? » Avec psycopg, un bloc with conn.transaction(): représente une transaction. Si le bloc se termine normalement, psycopg valide la transaction. Si une exception sort du bloc, psycopg annule la transaction. Ce test de tests/test_transactions.py vérifie exactement le cas du robot :

tests/test_transactions.py (part)
def test_a_failed_transaction_changes_nothing(db_url):
with psycopg.connect(db_url) as conn:
count = "SELECT message_count FROM tickets WHERE ticket_id = 'T-30002'"
before = conn.execute(count).fetchone()
with pytest.raises(psycopg.errors.CheckViolation):
with conn.transaction():
conn.execute(
"INSERT INTO messages (ticket_id, author, body)"
" VALUES ('T-30002', 'agent', 'First part')"
)
conn.execute(
"UPDATE tickets SET message_count = message_count + 1"
" WHERE ticket_id = 'T-30002'"
)
conn.execute(
"INSERT INTO messages (ticket_id, author, body)"
" VALUES ('T-30002', 'robot', 'x')"
)
after = conn.execute(count).fetchone()
n = conn.execute("SELECT count(*) FROM messages WHERE body = 'First part'").fetchone()[0]
assert after == before
assert n == 0

Dans le module 3, le point de terminaison « ajouter un message » de l’API utilise ce bloc pour l’insertion et le compte.

Un café a un tableau blanc : « Gâteau au chocolat : 3 pointes restantes ». Deux serveurs prennent une commande au même moment. Chacun lit « 3 », vend une pointe et écrit « 2 » sur le tableau. Le tableau indique maintenant 2, mais il reste seulement 1 pointe. Aucun des deux n’a fait d’erreur individuellement, et personne n’a vu de message d’erreur. Un des deux changements a disparu.

C’est une mise à jour perdue. Deux sessions lisent la même valeur, chacune calcule une nouvelle valeur, et la deuxième écriture remplace la première. Les transactions seules ne l’empêchent pas. Le changement de chaque serveur était « tout ou rien »; le problème vient du moment de la lecture.

Le projet contient un script qui reproduit ce problème sur T-30002. Il ouvre deux connexions réelles à la base de données, A et B. Chacune ajoute un à message_count comme le ferait un programme imprudent. Elle lit le compte, ajoute un en Python, puis réécrit le résultat. Le script exécute leurs étapes dans un ordre fixe pour produire le même résultat chaque fois. En situation réelle, l’ordre dépend du moment d’exécution. Le problème survient seulement parfois, ce qui le rend difficile à trouver. Le script change seulement le compte et le remet à sa valeur initiale à la fin.

Explorer

Deux sessions ajoutent un message à T-30002 en même temps

Étape
8 sur 8
message_count enregistré
2
Valeur finale attendue
3
Mises à jour perdues
1

Deux messages ont été ajoutés, mais le compte est 2 : 1 mise à jour perdue. Rien n’a échoué, et personne n’a vu d’erreur.

  • un changement pas encore enregistré
  • attend un verrou, ou un changement perdu
  • COMMIT : le changement est enregistré
Lire, puis écrire : billet T-30002, étape 8 sur 8
#Session ASession BEnregistré
1Début : message_count = 1 est enregistré1
2SELECT: message_count = 11
3SELECT: message_count = 11
4UPDATE … SET message_count = 2 (la valeur lue + 1)1
5COMMIT2
6UPDATE … SET message_count = 2 (la valeur lue + 1)2
7COMMIT2
8Fin : message_count = 2. Il devrait valoir 3.2

Enregistré avec python -m scripts.lost_update --json sur les petites données : deux vraies connexions à la base de données, exécutées dans un ordre fixe par le script (un cas construit; dans la réalité, l’ordre dépend du hasard).

Deux connexions réelles à la base de données, A et B, ajoutent chacune un à message_count, qui commence à 1. Les étapes et les valeurs viennent de la sortie de scripts/lost_update.py sur le petit jeu de données. Le script fixe l'ordre des étapes.
Description textuelle de l’explorateur

L’explorateur a une commande principale avec trois choix : lire, puis écrire; SELECT … FOR UPDATE; et une seule instruction UPDATE. Les boutons Next step (étape suivante), Back (retour) et All steps (toutes les étapes) permettent de parcourir les étapes. Une table montre les actions des sessions A et B à chaque étape, puis le message_count enregistré.

Lire, puis écrire (8 étapes) : A lit 1, B lit 1, A écrit 2 et valide, B écrit 2 et valide. Le compte enregistré passe de 1 à 2, puis reste à 2. Il devrait être 3 : une mise à jour est perdue, sans erreur.

SELECT … FOR UPDATE (9 étapes) : A lit 1 et verrouille la ligne. B demande la même ligne et attend. A écrit 2 et valide, ce qui libère le verrou. La lecture de B renvoie 2. B écrit 3 et valide. Le compte est 3; rien n’est perdu.

Une seule instruction UPDATE (7 étapes) : A exécute UPDATE … SET message_count = message_count + 1, qui produit 2 et verrouille la ligne. Le UPDATE de B attend. A valide. Le UPDATE de B utilise le 2 enregistré et produit 3. B valide. Le compte est 3; rien n’est perdu.

Le premier mode de l’explorateur correspond au café. B a lu 1 avant la validation de A. Son calcul « 1 + 1 » donne donc 2, et B remplace le 2 de A par 2.

L’idée : verrouiller la ligne ou laisser la base de données additionner

Section intitulée « L’idée : verrouiller la ligne ou laisser la base de données additionner »

Il existe deux solutions. Les deux utilisent un verrou de ligne. C’est une marque qu’une transaction place sur une ligne pour faire attendre les autres transactions qui veulent la changer. PostgreSQL verrouille chaque ligne qu’une transaction modifie, jusqu’au COMMIT ou au ROLLBACK.

Verrouillez avant de lire : SELECT … FOR UPDATE. Cette instruction lit la ligne et la verrouille en même temps. Une deuxième session qui demande la même ligne avec FOR UPDATE attend la fin de la première. Sa lecture renvoie ensuite la nouvelle valeur. C’est le serveur qui prend le marqueur et le garde. L’autre serveur doit alors attendre pour utiliser le tableau.

Laissez la base de données additionner : une seule instruction UPDATE. SET message_count = message_count + 1 lit et écrit en une étape, tout en gardant le verrou de ligne. Le programme ne conserve jamais un ancien nombre. Le UPDATE de B attend la validation de A, puis ajoute un à la valeur enregistrée.

Quelle solution choisir? Si SQL peut calculer la nouvelle valeur, utilisez une seule instruction : c’est plus court, sans lecture à oublier. Si le programme doit examiner la ligne avant de décider, utilisez FOR UPDATE. Le point de terminaison « ajouter un message » de l’API, dans le module 3, fait les deux. Il verrouille le billet avec SELECT status … FOR UPDATE, ce qui indique aussi si le billet existe. Ensuite, il insère le message et exécute un seul UPDATE avec message_count + 1. Jusqu’à la validation, aucun autre message ne peut modifier ce billet.

Les niveaux d’isolation, en mots simples. Le niveau par défaut de PostgreSQL est read committed (lecture des données validées). Chaque instruction voit les données validées au moment où elle commence. C’est pourquoi la session B a lu 1. Il existe des niveaux plus stricts. Avec BEGIN ISOLATION LEVEL REPEATABLE READ, la même commande au café ne perd pas la mise à jour. Mais l’écriture de B échoue :

ERROR: could not serialize access due to concurrent update

C’est une exécution réelle avec deux sessions psql. A a validé son changement en premier. Le UPDATE de B a donc refusé d’écraser une valeur que B n’avait pas vue. Rien n’est perdu, mais le programme doit intercepter l’erreur et exécuter sa transaction de nouveau. Le niveau le plus strict, SERIALIZABLE (sérialisable), fonctionne de la même façon. Ce cours utilise le niveau par défaut avec des verrous de ligne. Une attente est plus facile à gérer qu’une nouvelle tentative.

Quand utiliser les transactions et les verrous, et ce qu’ils coûtent

Section intitulée « Quand utiliser les transactions et les verrous, et ce qu’ils coûtent »

Utilisez une transaction pour chaque changement qui touche plusieurs lignes : un message et son compte, un remboursement et son enregistrement. Utilisez un verrou de ligne ou un seul UPDATE quand deux requêtes peuvent modifier la même ligne en même temps.

Ces outils ont un coût :

  • L’attente. Pendant que A garde un verrou, B attend. Une transaction qui garde longtemps un verrou fait attendre chaque requête visant cette ligne. Gardez les transactions courtes. N’attendez jamais une personne ou un service lent à l’intérieur d’une transaction. Gestion des connexions montre l’effet d’une longue transaction sur l’API.
  • Les nouvelles tentatives avec les niveaux d’isolation plus stricts : le programme doit intercepter l’erreur et recommencer.
  • La prudence avec plusieurs lignes. Si deux transactions verrouillent les deux mêmes lignes dans un ordre différent, chacune peut attendre l’autre. PostgreSQL détecte cet interblocage et arrête l’une d’elles avec une erreur. Verrouillez les lignes dans un ordre fixe, par exemple selon l’identifiant du billet. (Décrit ici, mais pas exécuté.)

Erreur courante

« Lire le compte, ajouter un, réécrire le résultat »

Symptôme : après l’ajout simultané de deux messages, le message_count de T-30002 est 2, pas 3. Aucune requête n’a échoué, et le journal ne contient aucune erreur. python -m scripts.lost_update le montre : Two messages were added: the count should be 3. It is 2: 1 update lost. (Deux messages ont été ajoutés : le compte devrait être 3. Il est 2 : une mise à jour perdue.)

Cause : les deux sessions ont lu 1 avant toute validation. Chacune a calculé 2 dans le programme, et la deuxième écriture a remplacé la première. Une transaction autour de chaque session ne règle rien, car le problème vient du moment de la lecture.

Diagnostic : cherchez le code qui lit une valeur, la modifie dans le programme et réécrit le résultat, sans verrou. Comparez la copie avec la source. La requête de cette leçon trouve chaque billet dont le message_count diffère du nombre de messages.

Correction : laissez la base de données calculer la valeur dans un seul UPDATE (message_count = message_count + 1). Ou lisez avec SELECT … FOR UPDATE dans la même transaction que l’écriture. tests/test_transactions.py conserve les trois versions sous forme de tests.

  1. Copiez le script et le dernier fichier de tests depuis l’instantané :

    Fenêtre de terminal
    cp ../nextia-learning-labs/C20/snapshots/end-of-m02/scripts/lost_update.py scripts/
    cp ../nextia-learning-labs/C20/snapshots/end-of-m02/tests/test_transactions.py tests/

    Sur Windows, dans PowerShell : Copy-Item ..\nextia-learning-labs\C20\snapshots\end-of-m02\scripts\lost_update.py scripts\, puis faites la même chose pour tests\test_transactions.py vers tests\.

  2. Reproduisez la mise à jour perdue :

    Fenêtre de terminal
    python -m scripts.lost_update
    - start message_count = 1
    A read message_count = 1
    B read message_count = 1
    A write message_count = 2
    A COMMIT
    B write message_count = 2
    B COMMIT
    - end message_count = 2
    Two messages were added: the count should be 3. It is 2: 1 update lost. (0.30 s)

    Le nombre de secondes à la fin diffère sur votre ordinateur. Si le compte ne commence pas à 1, lancez d’abord python -m scripts.load --reset.

  3. Exécutez le script avec un verrou de ligne :

    Fenêtre de terminal
    python -m scripts.lost_update --lock
    - start message_count = 1
    A read (lock the row) message_count = 1
    B read: waits, because A has locked the row
    A write message_count = 2
    A COMMIT
    B read (after A's commit) message_count = 2
    B write message_count = 3
    B COMMIT
    - end message_count = 3
    Two messages were added: the count should be 3. It is 3. (0.53 s)

    Cette exécution est plus longue, car le script attend une demi-seconde pour vérifier si B est bloquée.

  4. Exécutez le script avec une seule instruction UPDATE :

    Fenêtre de terminal
    python -m scripts.lost_update --atomic
    - start message_count = 1
    A UPDATE ... SET message_count = message_count + 1 (locks the row)
    B UPDATE ... SET message_count = message_count + 1: waits, because A has locked the row
    A COMMIT
    B the UPDATE runs now, on A's committed value
    B COMMIT
    - end message_count = 3
    Two messages were added: the count should be 3. It is 3. (0.55 s)
  5. Exécutez tous les tests :

    Fenêtre de terminal
    python -m pytest
    tests/test_classify.py .................. [ 33%]
    tests/test_contract.py .. [ 37%]
    tests/test_database.py ... [ 43%]
    tests/test_history.py ........ [ 58%]
    tests/test_load.py ... [ 64%]
    tests/test_schema.py ............... [ 92%]
    tests/test_transactions.py .... [100%]
    ============================== 53 passed in 2.65s ==============================

    Seule la fin de la sortie est affichée, et votre durée diffère. Votre projet correspond maintenant à l’instantané end-of-m02.

Exercice du module : une table plusieurs-à-plusieurs et ses règles

Section intitulée « Exercice du module : une table plusieurs-à-plusieurs et ses règles »

Créez la table ticket_citations conçue dans Entités et relations, puis vérifiez ses règles dans psql :

  1. Créez la table, avec une clé primaire sur les trois colonnes et les deux clés étrangères.
  2. Insérez deux citations valides pour T-30002 : refunds-and-payments version 3 et refund-approval-procedure version 2.
  3. Insérez trois lignes invalides et prévoyez d’abord chaque erreur : la même citation, une citation pour T-99999 et returns-policy version 5.
  4. Supprimez la table avec DROP TABLE ticket_citations; pour que votre projet reste identique à l’instantané.
Afficher un exemple de réponse
CREATE TABLE ticket_citations (
ticket_id text NOT NULL REFERENCES tickets (ticket_id) ON DELETE CASCADE,
doc_id text NOT NULL,
doc_version integer NOT NULL,
PRIMARY KEY (ticket_id, doc_id, doc_version),
FOREIGN KEY (doc_id, doc_version) REFERENCES documents (doc_id, version)
);
INSERT INTO ticket_citations VALUES ('T-30002', 'refunds-and-payments', 3);
INSERT INTO ticket_citations VALUES ('T-30002', 'refund-approval-procedure', 2);
INSERT INTO ticket_citations VALUES ('T-30002', 'refunds-and-payments', 3);
INSERT INTO ticket_citations VALUES ('T-99999', 'refunds-and-payments', 3);
INSERT INTO ticket_citations VALUES ('T-30002', 'returns-policy', 5);
SELECT * FROM ticket_citations;
DROP TABLE ticket_citations;
CREATE TABLE
INSERT 0 1
INSERT 0 1
ERROR: duplicate key value violates unique constraint "ticket_citations_pkey"
DETAIL: Key (ticket_id, doc_id, doc_version)=(T-30002, refunds-and-payments, 3) already exists.
ERROR: insert or update on table "ticket_citations" violates foreign key constraint "ticket_citations_ticket_id_fkey"
DETAIL: Key (ticket_id)=(T-99999) is not present in table "tickets".
ERROR: insert or update on table "ticket_citations" violates foreign key constraint "ticket_citations_doc_id_doc_version_fkey"
DETAIL: Key (doc_id, doc_version)=(returns-policy, 5) is not present in table "documents".
ticket_id | doc_id | doc_version
-----------+---------------------------+-------------
T-30002 | refunds-and-payments | 3
T-30002 | refund-approval-procedure | 2
(2 rows)
DROP TABLE

PostgreSQL a donné un nom aux règles que vous n’avez pas nommées, comme ticket_citations_ticket_id_fkey. La version 5 de la politique de retour n’existe pas (la plus récente est la 4). La clé étrangère sur deux colonnes la refuse donc. Une clé étrangère sur doc_id seulement ne pourrait pas détecter ce problème.

Pour aller plus loin : PostgreSQL peut aussi annuler un CREATE TABLE. Placez tout l’exercice entre BEGIN; et ROLLBACK;. Ensuite, \d ticket_citations affiche Did not find any relation named "ticket_citations". (Aucune relation nommée « ticket_citations » trouvée.) Dans la transaction, la première erreur arrête tout ce qui suit. Testez donc une ligne invalide par transaction.

Vous avez terminé le module quand vous pouvez montrer ces résultats dans votre propre projet :

  • python -m pytest affiche 53 passed (53 tests réussis). tests/test_schema.py prouve que la base de données rejette elle-même les doublons, les lignes orphelines et les valeurs impossibles. L’API n’intervient pas.
  • Vous pouvez dessiner le diagramme entité-relation du service de soutien et expliquer la décision à chaque extrémité d’une ligne. Un exemple : les exécutions d’IA conservent leur coût.
  • Vous pouvez expliquer pourquoi la migration 005 garde trois messages dans orphaned_messages et ce que disait l’erreur du premier brouillon.
  • Avec la sortie de python -m scripts.lost_update, vous pouvez expliquer pourquoi une mise à jour a été perdue. Vous pouvez aussi expliquer pourquoi --lock et --atomic n’en perdent aucune.

Réinitialisation. Si votre projet ne fonctionne plus, renommez-le et copiez l’instantané end-of-m02, comme l’indique le README des instantanés. Dans ~/projects :

Fenêtre de terminal
mv ticket-api ticket-api-old
cp -R nextia-learning-labs/C20/snapshots/end-of-m02 ticket-api
cp ticket-api-old/.env ticket-api/

Sur Windows, dans PowerShell : Rename-Item ticket-api ticket-api-old, puis Copy-Item -Recurse nextia-learning-labs\C20\snapshots\end-of-m02 ticket-api, puis Copy-Item ticket-api-old\.env ticket-api\. Recréez l’environnement virtuel, puis lancez python -m scripts.load --reset et python -m pytest : 53 passed. Ces étapes ont été testées sur macOS. Les commandes Windows n’ont pas été testées.

Vérifiez votre compréhension

Une transaction insère un message, ajoute un à message_count, puis échoue à cause d'une règle. Que reste-t-il dans la base de données, et qu'a affiché psql pour COMMIT?

Afficher la réponse

Rien de la transaction : aucun nouveau message, et message_count reste inchangé. Après l’erreur, PostgreSQL a ignoré chaque instruction (current transaction is aborted, la transaction courante est annulée). Le COMMIT (valider) a été transformé en annulation, donc psql a affiché ROLLBACK (annuler). C’est l’atomicité : tout le changement ou rien.

Vérifiez votre compréhension

Trois sessions ajoutent un message à un billet de soutien dont le compteur vaut 4. Chacune lit le compteur, ajoute un en Python et écrit le résultat. Les trois lisent avant qu'une session valide sa transaction. Quelle peut être la valeur finale du compteur? Quelle devrait-elle être?

Afficher la réponse

Elle devrait être 7. Mais chaque session lit 4 et écrit 5, donc le compteur termine à 5 : deux mises à jour perdues, sans erreur. Selon le moment des opérations, le résultat peut être 5, 6 ou 7. C’est pourquoi le bogue apparaît seulement parfois. Avec message_count = message_count + 1 dans une seule instruction, ou SELECT … FOR UPDATE avant la lecture, c’est toujours 7.

Vérifiez votre compréhension

Imaginez une table de remboursements (elle n'est pas dans le projet). Un agent peut ajouter un remboursement de courtoisie à un billet seulement si le total des remboursements reste à 50 dollars ou moins. Le programme lit le total, vérifie la limite en Python, puis insère le remboursement. Deux agents ajoutent chacun 20 dollars au même moment à un billet qui a déjà 30 dollars de remboursements. Que peut-il arriver, et quelle protection convient?

Afficher la réponse

Les deux lisent 30 et voient que 30 + 20 respecte la limite. Les deux insèrent le remboursement : le total devient 70, sans erreur. C’est la même erreur qu’une mise à jour perdue : une décision basée sur une valeur qui a changé après la lecture. Un seul UPDATE (mettre à jour) ne convient pas, car le programme insère une ligne dans une autre table après une vérification. Verrouillez d’abord le billet, dans la même transaction : SELECT … FROM tickets WHERE ticket_id = … FOR UPDATE. Le deuxième agent attend, puis lit 50, et le programme refuse le remboursement.

Récapitulatif

  • Une transaction regroupe plusieurs instructions en un seul changement : COMMIT enregistre le tout, et ROLLBACK, ou une erreur, annule le tout. La réponse du robot n’a laissé aucun changement partiel.
  • ACID : atomique, cohérente (les règles sont respectées), isolée (personne ne voit les changements inachevés) et durable (un changement validé survit à une panne).
  • Une mise à jour perdue survient quand deux sessions lisent la même valeur et que la deuxième écriture remplace la première : 2 au lieu de 3, sans erreur.
  • Un verrou de ligne l’empêche. Utilisez un seul UPDATE … SET n = n + 1 quand SQL peut calculer la valeur. Utilisez SELECT … FOR UPDATE quand le programme doit d’abord lire. Le niveau d’isolation par défaut, lecture validée, exige l’une de ces méthodes. Les niveaux plus stricts refusent l’écriture, et le programme doit réessayer.
  • Votre projet correspond à end-of-m02 : 53 passed (53 tests réussis). Le module 3 appelle la base de données depuis l’API : paramètres, pool de connexions (pool) et migrations plus sûres.

Leçon suivante : Requêtes sûres