</>
Plan du cours — 55/64

Transactions dans le code

Avancé

Une transaction doit être courte, cohérente et réessayable.

await using var conn = await db.OpenConnectionAsync(ct);
await using var tx = await conn.BeginTransactionAsync(ct);

await using (var debit = new NpgsqlCommand(
    "UPDATE comptes SET solde = solde - $1 WHERE id = $2 AND solde >= $1", conn, tx))
{
    debit.Parameters.AddWithValue(montant);
    debit.Parameters.AddWithValue(source);
    if (await debit.ExecuteNonQueryAsync(ct) == 0) throw new SoldeInsuffisantException();
}
await using (var credit = new NpgsqlCommand(
    "UPDATE comptes SET solde = solde + $1 WHERE id = $2", conn, tx))
{
    credit.Parameters.AddWithValue(montant);
    credit.Parameters.AddWithValue(destination);
    await credit.ExecuteNonQueryAsync(ct);
}

await tx.CommitAsync(ct);

Règles à retenir#

  • Aucun appel externe (HTTP, e-mail, file de messages) entre BEGIN et COMMIT : la transaction retient des verrous pendant toute l'attente.
  • Vérifier le nombre de lignes affectées : UPDATE ... WHERE solde >= $1 échoue silencieusement (0 ligne) si la condition est fausse.
  • Réessayer l'ensemble de la transaction sur 40001 (sérialisation) et 40P01 (deadlock), avec un nombre d'essais borné et un délai croissant.
  • Propager le CancellationToken : une requête annulée doit annuler la requête SQL.

Concurrence optimiste#

Pour éviter les mises à jour perdues sans verrouiller, on utilise une colonne de version :

UPDATE produits
SET    prix = $1, version = version + 1
WHERE  id = $2 AND version = $3;      -- 0 ligne affectée : quelqu'un d'autre a modifié entre-temps

EF Core ([ConcurrencyCheck], RowVersion) et la plupart des ORM offrent ce mécanisme.

Astuce : préférez les opérations atomiques en une instruction (UPDATE ... SET stock = stock - 1 WHERE stock > 0) aux séquences lire-calculer-écrire dans le code : elles sont plus courtes, plus rapides et sans course.