Module global · ORM_CSHX2 · Transactions

ORM_Transactions

Gestion des transactions SQL au sein du framework ORM_CSHX2. Trois helpers globaux — ORM_TransactionBegin, ORM_TransactionCommit, ORM_TransactionRollBack — encapsulent un modèle « transaction owner » qui rend les appels imbriqués sûrs sans intervention de l'appelant.

PUBLIQUE TRANSACTION MySQL · MariaDB · PostgreSQL
01

📋 Description

Une transaction SQL délimite un ensemble d'opérations qui doivent réussir ou échouer en bloc. Soit toutes les modifications sont appliquées (COMMIT), soit aucune ne l'est (ROLLBACK). C'est la garantie d'atomicité du quatuor ACID.

ORM_CSHX2 expose trois procédures globales pour piloter une transaction :

ProcédureRôle
ORM_TransactionBegin()Ouvre une transaction sur la connexion active.
ORM_TransactionCommit()Valide la transaction (les modifications deviennent permanentes).
ORM_TransactionRollBack()Annule la transaction (les modifications sont perdues).

Ces trois procédures s'appuient sur un singleton qui mémorise l'état de la transaction au niveau du processus. Un seul niveau de transaction est actif à un instant donné — les appels imbriqués sont gérés par le pattern transaction owner détaillé dans la section suivante.

🗄️ Providers supportés

MySQL · MariaDB — utilise SQLTransaction(sqlDébut / sqlFin / sqlAnnule).

PostgreSQL — utilise SQLTransaction pour le BEGIN, puis exécute COMMIT / ROLLBACK en SQL natif. Un SET LOCAL lock_timeout est appliqué automatiquement à l'ouverture (sauf configuration contraire) pour éviter les attentes infinies sur les verrous.

02

🎯 Modèle "transaction owner" & transactions en cascade

Le modèle transaction owner est la pierre angulaire du système. Il répond à un problème courant en programmation modulaire : quand une procédure A qui ouvre une transaction appelle une procédure B qui veut elle-même ouvrir une transaction, que se passe-t-il ?

Dans ORM_CSHX2, la règle est simple :

Le premier appelant à ouvrir une transaction en devient le propriétaire (« owner »). Son nom est mémorisé en interne.

Les appels imbriqués à ORM_TransactionBegin sont silencieusement ignorés — ils ne déclenchent pas de nouvelle transaction (les bases SQL ne supportent pas les transactions imbriquées de toute façon).

Seul le propriétaire peut clôturer la transaction via ORM_TransactionCommit ou ORM_TransactionRollBack. Les appels par d'autres procédures à ces clôtures sont également silencieusement ignorés.

Conséquence pratique : chaque procédure peut être écrite comme si elle gérait sa propre transaction. Si elle est appelée seule, la transaction est bien ouverte et fermée par elle. Si elle est appelée à l'intérieur d'une transaction parente, ses appels à Begin/Commit/Rollback sont neutralisés et c'est l'appelant racine qui pilote.

Exemple de cascade

Imaginons trois procédures imbriquées :

┌─ ProcédureA ──── ORM_TransactionBegin() → OK : devient OWNER │ │ ┌─ ProcédureB ── ORM_TransactionBegin() → no-op (transaction déjà active) │ │ │ │ ┌─ ProcédureC ─ ORM_TransactionBegin() → no-op │ │ │ ... INSERT/UPDATE/DELETE ... │ │ └─ ORM_TransactionCommit() → no-op (C n'est pas owner) │ │ │ └─ ORM_TransactionCommit() → no-op (B n'est pas owner) │ └─ ORM_TransactionCommit() → OK : COMMIT effectif (A est owner)

L'ouverture est faite une seule fois en réalité (premier appel de A). La fermeture est faite une seule fois en réalité (dernier appel de A, qui est l'owner). Tous les appels intermédiaires sont neutralisés sans alerter le programme.

⚠️ Owner mismatch silencieux sur Commit/Rollback

Quand ORM_TransactionCommit() ou ORM_TransactionRollBack() est appelé par une procédure qui n'est pas l'owner de la transaction active, l'appel est silencieusement ignoré :

bProcessing reste à Vrai

nErrorCode reste à 0

• Aucune erreur n'est émise

C'est volontaire et nécessaire pour que le pattern owner fonctionne en cascade. Mais ça peut piéger : si une procédure imbriquée croit avoir clos la transaction et l'appelant continue à modifier la base, ces modifications seront incluses dans le COMMIT (ou perdues dans le ROLLBACK) que fera l'owner racine plus tard.

✅ Bonne pratique — toujours appeler Begin/Commit/Rollback dans la même procédure

Pour éviter toute surprise, une procédure qui appelle ORM_TransactionBegin() doit toujours appeler ORM_TransactionCommit() ou ORM_TransactionRollBack() elle-même, dans tous les chemins de retour. Si la transaction est déjà active, ces appels sont des no-op gratuits ; sinon, ils ferment la transaction proprement.

03

🔑 Méthodes

ORM_TransactionBegin

Déclaration
PROCÉDURE ORM_TransactionBegin() : (booléen, entier, chaîne)

Ouvre une transaction sur la connexion active si aucune n'est déjà en cours. Si une transaction est déjà active, l'appel est silencieusement ignoré (no-op) — voir §02 Modèle owner.

Retour : triplet standard ORM (bProcessing, nErrorCode, sErrorMessage).

ORM_TransactionCommit

Déclaration
PROCÉDURE ORM_TransactionCommit() : (booléen, entier, chaîne)

Valide la transaction en cours (COMMIT). Si l'appelant n'est pas l'owner, l'appel est silencieusement ignoré.

Retour : triplet standard ORM (bProcessing, nErrorCode, sErrorMessage).

ORM_TransactionRollBack

Déclaration
PROCÉDURE ORM_TransactionRollBack()

Annule la transaction en cours (ROLLBACK). Si l'appelant n'est pas l'owner, l'appel est silencieusement ignoré.

⚠️ ORM_TransactionRollBack est une procédure void

Contrairement aux deux autres helpers, ORM_TransactionRollBack() ne retourne aucune valeur. L'appel correct est :

ORM_TransactionRollBack()

Et non :

(bProcessing, nErrorCode, sErrorMessage) = ORM_TransactionRollBack() // ❌

Cette signature reflète la réalité opérationnelle : un ROLLBACK arrive le plus souvent en aval d'une erreur, et il n'y a généralement rien d'utile à faire si lui-même échoue.

04

⚙️ Comportements observables

PostgreSQL — verrou avec timeout automatique

Sur PostgreSQL, ORM_TransactionBegin() applique automatiquement un SET LOCAL lock_timeout au démarrage de la transaction. Cela évite qu'une opération bloquée sur un verrou attende indéfiniment : passé le délai, l'opération échoue avec une erreur claire plutôt que de figer le poste.

Le délai par défaut est défini par la constante interne du framework. Il peut être désactivé ponctuellement via la propriété m_bNO_WAIT du singleton de transaction (cas spécifiques où l'attente est souhaitée).

📖 PostgreSQL — lock_timeout

Audit — traçabilité de la procédure démarreuse

Lors de l'ouverture d'une transaction, ORM_TransactionBegin() auto-détecte le nom de la procédure appelante et le mémorise au niveau de la session. Ce nom est ensuite écrit dans les colonnes framework SQL_PROCEDURE_INSERT et SQL_PROCEDURE_UPDATE par les méthodes mth_Enregistrer et apparentées, fournissant une traçabilité complète : quelle procédure métier a démarré l'unité de travail qui a inséré ou modifié cet enregistrement ?

Mode verbose — traces des transactions

Lorsque la session ORM_CSHX2 est en mode verbose, chaque ouverture, validation ou annulation génère une trace lisible :

// Exemples de traces produites en mode verbose 🔁 BEGIN MaProcédure ✅ COMMIT MaProcédure ↩️ ROLLBACK MaProcédure

Ces traces facilitent le débogage des cascades de transactions complexes en montrant clairement qui ouvre, qui valide et qui annule.

05

⚠️ Gestion des erreurs

Codes d'erreur retournés par les helpers ORM_TransactionBegin et ORM_TransactionCommit. ORM_TransactionRollBack étant void, ses erreurs ne sont pas remontées à l'appelant.

CodeConstanteCondition
-30099ERR_TXN_LOCK_TIMEOUT_SQLPostgreSQL uniquement — échec du SET LOCAL lock_timeout appliqué après le BEGIN. La transaction est annulée, l'état revient à « pas de transaction active ».
-30098ERR_TXN_INIT_SQLLe BEGIN SQL a échoué (problème de connexion, droits insuffisants, etc.).
-30097ERR_TXN_COMMIT_SQLPostgreSQL uniquement — la requête COMMIT a échoué.
-30096ERR_TXN_COMMIT_ECHECMySQL/MariaDB — l'appel SQLTransaction(sqlFin) a échoué.
-30093ERR_TXN_INACTIVEORM_TransactionCommit() appelé alors qu'aucune transaction n'est active. Indique généralement un déséquilibre Begin/Commit côté appelant.
ℹ️ Les no-op silencieux ne sont pas des erreurs

Quand un appel imbriqué à Begin, Commit ou Rollback est ignoré au titre du pattern owner, aucune erreur n'est remontée : bProcessing = Vrai, nErrorCode = 0. Le code ERR_TXN_INACTIVE ne couvre que le cas où aucune transaction n'est active du tout au moment du Commit — pas le cas du caller ≠ owner.

06

💡 Exemples

Mode 1 — transaction simple

// ── Modification atomique : tout ou rien ──────────────────────── (bProcessing, nErrorCode, sErrorMessage) = ORM_TransactionBegin() SI bProcessing ALORS // ... une ou plusieurs opérations SQL ... (bProcessing, nErrorCode, sErrorMessage,) = clClient:mth_Enregistrer() SI bProcessing ALORS // ✅ Tout est OK — on valide (bProcessing, nErrorCode, sErrorMessage) = ORM_TransactionCommit() SI bProcessing = Faux ALORS Erreur(sErrorMessage) FIN SINON // ❌ Échec — on annule, aucune modification en base ORM_TransactionRollBack() FIN SINON Erreur(sErrorMessage) FIN

Mode 2 — transactions en cascade (procédures imbriquées)

Chaque procédure est écrite comme si elle gérait sa propre transaction. Lors de l'imbrication, seul le démarreur racine pilote effectivement.

// ── Procédure parente : pilote la transaction ─────────────────── PROCÉDURE CommandeComplete() LOCAL bProcessing est un booléen = Vrai nErrorCode est un entier sErrorMessage est une chaîne (bProcessing, nErrorCode, sErrorMessage) = ORM_TransactionBegin() // ✅ devient OWNER SI bProcessing ALORS (bProcessing, nErrorCode, sErrorMessage) = EnregistrerLignes() SI bProcessing ALORS (bProcessing, nErrorCode, sErrorMessage) = ORM_TransactionCommit() // ✅ COMMIT effectif SINON ORM_TransactionRollBack() // ❌ ROLLBACK effectif FIN FIN // ── Procédure imbriquée : appel "défensif" — neutralisé en cascade ─ PROCÉDURE EnregistrerLignes() LOCAL bProcessing est un booléen = Vrai nErrorCode est un entier sErrorMessage est une chaîne // Si appelée seule → ouvre une vraie transaction // Si appelée depuis CommandeComplete → no-op (transaction déjà active) (bProcessing, nErrorCode, sErrorMessage) = ORM_TransactionBegin() SI bProcessing ALORS // ... INSERT/UPDATE des lignes ... SI bProcessing ALORS (bProcessing, nErrorCode, sErrorMessage) = ORM_TransactionCommit() // no-op si imbriquée SINON ORM_TransactionRollBack() // no-op si imbriquée FIN FIN
ℹ️ Pourquoi cette robustesse compte

La procédure EnregistrerLignes peut être appelée depuis n'importe où — un bouton, un script de migration, une autre procédure orchestratrice. Son code est identique dans tous les cas. C'est l'environnement d'appel qui détermine si Begin/Commit/Rollback ont un effet réel ou non, sans aucun branchement conditionnel à écrire.

Mode 3 — méthodes qui gèrent leur propre transaction

Certaines méthodes du framework — comme mth_EffacerSelonID — ouvrent et ferment leur propre transaction si aucune n'est active à l'appel. Si l'appelant veut grouper plusieurs opérations dans la même unité de travail, il doit ouvrir lui-même la transaction parente :

// ── Cas 1 : suppression isolée — pas besoin de transaction côté appelant clClient:mth_EffacerSelonID(42) // La méthode ouvre, exécute, et ferme sa propre transaction. // ── Cas 2 : suppression dans un lot atomique — transaction parente (bProcessing, nErrorCode, sErrorMessage) = ORM_TransactionBegin() SI bProcessing ALORS (bProcessing, nErrorCode, sErrorMessage) = clClient:mth_EffacerSelonID(42) // → mth_EffacerSelonID détecte la transaction parente et ne touche pas à la transaction SI bProcessing ALORS (bProcessing, nErrorCode, sErrorMessage) = clClient:mth_EffacerSelonID(43) FIN SI bProcessing ALORS (bProcessing, nErrorCode, sErrorMessage) = ORM_TransactionCommit() SINON ORM_TransactionRollBack() // ❌ aucun des 2 effacements n'est conservé FIN FIN

Mode 4 — verrou pessimiste sur SELECT puis modification atomique

// ── Verrouiller un enregistrement, le modifier, valider ───────── (bProcessing, nErrorCode, sErrorMessage) = ORM_TransactionBegin() SI bProcessing ALORS // 🔒 SELECT avec verrou pessimiste (interdit hors transaction) (bProcessing, nErrorCode, sErrorMessage) = clClient:mth_ChargerSelonID(42, LockForUpdate) SI bProcessing ET clClient:p_nOccurrencesTrouvées = 1 ALORS clClient:m_DENOMINATION = "Nouveau nom" (bProcessing, nErrorCode, sErrorMessage,) = clClient:mth_Enregistrer() FIN SI bProcessing ALORS (bProcessing, nErrorCode, sErrorMessage) = ORM_TransactionCommit() SINON ORM_TransactionRollBack() FIN FIN
07

🔗 Méthodes qui gèrent leur propre transaction

Les méthodes suivantes du framework ouvrent une transaction locale si aucune n'est active à l'appel, puis la valident ou l'annulent automatiquement. Si une transaction parente est déjà active, elles ne touchent pas à la transaction et c'est l'appelant qui pilote.

MéthodeEffet
mth_EffacerSelonIDSuppression d'un enregistrement et de ses dépendances. Transaction locale ouverte si aucune transaction parente n'est active.

Pour les opérations isolées (un seul appel), aucune action côté appelant n'est nécessaire. Pour grouper plusieurs opérations dans une seule unité de travail, ouvrir explicitement une transaction parente avec ORM_TransactionBegin() avant les appels.