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);

💡STRJRNPF doit ê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, BeginTransaction requiert obligatoirement IsolationLevel.ReadCommitted. Sans ce paramètre, le contrôle d'engagement n'est pas initialisé côté IBM i et les appels à Commit ou Rollback n’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 ?