Transactions IBM i (AS/400) en C# (.NET) : Commit et Rollback avec NTi
Introduction
Une transaction garantit qu’un ensemble d’opérations en base de données s’exécute de manière atomique: soit toutes les opérations sont validées Commit, soit aucune Rollback.
Le cas typique est le virement bancaire: débiter un compte et en créditer un autre sont deux opérations distinctes. Si le débit passe mais que le crédit échoue, les données sont corrompues.
Sur IBM i, cette mécanique repose sur le contrôle d’engagement DB2 for i. Lorsqu’un niveau d’isolation est spécifié, l’IBM i initialise automatiquement l’environnement de contrôle d’engagement. C’est lui qui permet de garantir qu’une transaction soit intégralement validée ou intégralement annulée, y compris en cas de terminaison anormale du programme
Pour que ce mécanisme fonctionne, chaque table doit être explicitement journalisée via STRJRNPF. Sans journalisation, les opérations s'exécutent mais le contrôle d'engagement ne s'applique pas à ces tables.
Cela passe par trois objets IBM i :
- Un récepteur de journal
CRTJRNRCV: le fichier physique où l’IBM i enregistre chaque modification. - Un journal
CRTJRN: l’objet logique qui pointe vers ce récepteur. - La journalisation des tables
STRJRNPF: pour chaque table concernées par les transactions.
Étape 1 - Préparer l'environnement IBM i
Ce tutoriel montre comment implémenter des transactions en C# (.NET) avec NTi et s’appuie sur un scenario de virement bancaire entre deux comptes. La table ACCOUNTS contient deux titulaires, Alice (1000€) et Bob (500€).
Les tests consistent à débiter l’un et créditer l’autre dans une même transaction.
Exécutez ce script complet dans ACS:
-- Créer la bibliothèque
CL: CRTLIB LIB(BANKTEST) TEXT('Demo transactions NTi');
-- Créer le récepteur de journal
CL: CRTJRNRCV JRNRCV(BANKTEST/BANKRCV) TEXT('Récepteur journal BANKTEST');
-- Créer le journal
CL: CRTJRN JRN(BANKTEST/BANKJRN) JRNRCV(BANKTEST/BANKRCV) TEXT('Journal BANKTEST');
-- Créer la table
SET CURRENT SCHEMA = BANKTEST;
CREATE TABLE accounts (
account_id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY,
owner VARCHAR(50) NOT NULL,
balance DECIMAL(11,2) NOT NULL DEFAULT 0,
CONSTRAINT pk_accounts PRIMARY KEY (account_id)
);
-- Données initiales
INSERT INTO accounts (owner, balance) VALUES ('Alice', 1000.00);
INSERT INTO accounts (owner, balance) VALUES ('Bob', 500.00);
-- Journaliser la table (obligatoire pour Commit/Rollback)
CL: STRJRNPF FILE(BANKTEST/ACCOUNTS) JRN(BANKTEST/BANKJRN);
💡
STRJRNPFdoit être exécuté sur chaque table qui participe à des transactions.
Pour vérifier que le journal a bien été créé:
WRKOBJ OBJ(BANKTEST/*ALL) OBJTYPE(*JRN);Étape 2 - Créer le projet .NET
Créez un projet Console App et ajoutez le package NTi:
dotnet new console -n NtiTransacDemo
cd NtiTransacDemo
dotnet add package Aumerial.Data.NtiÉtape 3 - Ouvrir la connexion
Déclarez une instance de NTiConnection et ouvrez la connexion :
using Aumerial.Data.Nti;
using System.Data;
using var conn = new NTiConnection("server=serverName;user=userName;password=password;schema=BANKTEST;");
conn.Open();
Console.WriteLine("Connexion OK");
💡 Avec NTi,
BeginTransactionrequiert obligatoirementIsolationLevel.ReadCommitted. Sans ce paramètre, le contrôle d'engagement n'est pas initialisé côté IBM i et les appels àCommitouRollbackn’ont aucun effet.
Étape 4 - Test 1 : Commit
Virement de 200€ d’Alice vers Bob. Les deux UPDATE sont exécutés dans la même transaction et validés ensemble par un Commit.
using var transaction = (NTiTransaction)conn.BeginTransaction(IsolationLevel.ReadCommitted);
try
{
var cmd1 = conn.CreateCommand();
cmd1.Transaction = transaction;
cmd1.CommandText = "UPDATE BANKTEST.ACCOUNTS SET BALANCE = BALANCE - 200 WHERE OWNER = 'Alice'";
cmd1.ExecuteNonQuery();
var cmd2 = conn.CreateCommand();
cmd2.Transaction = transaction;
cmd2.CommandText = "UPDATE BANKTEST.ACCOUNTS SET BALANCE = BALANCE + 200 WHERE OWNER = 'Bob'";
cmd2.ExecuteNonQuery();
transaction.Commit();
Console.WriteLine("Commit OK");
}
catch (Exception ex)
{
transaction.Rollback();
Console.WriteLine($"Rollback : {ex.Message}");
}
Pendant la transaction, les modifications sont visibles depuis ACS. Une fois le Commit appliqué, elles sont écrites définitivement en base, et ne peuvent plus être annulées. Sans Commit, un Rollback ou une terminaison anormale du programme annule toutes les modifications et remet les soldes dans leur état initial.
Vérifiez dans ACS:
SELECT OWNER, BALANCE FROM BANKTEST.ACCOUNTS;
-- Résultat attendu : Alice 800.00 / Bob 700.00Étape 5 - Test 2 : Rollback sur erreur
Tentative de débit de 9999 € sur le compte de Bob, cet exemple simule un virement refusé pour solde insuffisant. L’exception est levée manuellement pour reproduire ce cas, le Rollback est déclenché dans le catch et aucune modification n’est appliquée côté base de donnée.
using var transaction2 = (NTiTransaction)conn.BeginTransaction(IsolationLevel.ReadCommitted);
try
{
var cmd1 = conn.CreateCommand();
cmd1.Transaction = transaction2;
cmd1.CommandText = "UPDATE BANKTEST.ACCOUNTS SET BALANCE = BALANCE - 9999 WHERE OWNER = 'Bob'";
cmd1.ExecuteNonQuery();
throw new Exception("Solde insuffisant");
transaction2.Commit();
}
catch (Exception ex)
{
transaction2.Rollback();
Console.WriteLine($"Rollback : {ex.Message}");
}
Vérifiez dans ACS :
SELECT OWNER, BALANCE FROM BANKTEST.ACCOUNTS;
-- Résultat attendu : Alice 800.00 / Bob 700.00 (inchangé)Et maintenant ?
- Exécuter une commande CL - exécuter une commande CL et gérer les erreurs
- Appeler un programme - appel de programme RPG avec paramètres d'entrée/sortie
- Procédure stockée - appel de procédure stockée SQL avec Dapper et DataReader