Requêtes SQL sur IBM i, AS/400 en C# (.NET) avec NTi : SELECT, INSERT, UPDATE, DELETE

Introduction

Accéder aux données DB2 for i depuis une application .NET suit exactement le même modèle que n'importe quelle autre base de données avec NTi Data Provider, le connecteur ADO.NET natif pour IBM i. Pas de driver ODBC, pas de configuration système, un simple package NuGet suffit.

Ce tutoriel couvre toutes les opérations SQL essentielles depuis C# avec NTi: lecture, écriture, requêtes paramétrées, et les différentes approches disponibles selon vos besoins:

  • ADO .NET avec NTiCommand : contrôle total sur l’exécution des requêtes
  • Dapper : un micro-ORM léger qui s’intègre nativement à NTi pour un code plus concis et un mapping automatique vers vos objets C#

Les exemples s’appuient sur un schéma RETAIL contenant deux tables:

ARTICLES

  • REF - VARCHAR(20) - Product reference
  • LIBELLE - VARCHAR(100) - Product label
  • STOCK - INTEGER - Stock Quantity
  • PRIX_UNIT - DECIMAL(10,2) - Unit Price

CLIENTS

  • ID_CLIENT - INTEGER - Customer ID
  • NOM - VARCHAR(100) - Customer name
  • VILLE - VARCHAR(50) - City
  • PAYS - VARCHAR(50) - Country

Étape 1 - Installer NTi et Dapper

dotnet add package Aumerial.Data.Nti
dotnet add package Dapper

Étape 2 - Ouvrir la connexion

using Aumerial.Data.Nti;
using System.Data;
using Dapper;

using var conn = new NTiConnection("server=serverName;user=userName;password=password;");
conn.Open();

💡 La connexion NTi s'ouvre comme n'importe quel provider ADO.NET. Elle est compatible nativement avec Dapper sans aucune configuration supplémentaire.


Approche classique - ADO.NET avec NTiCommand

NTiCommand est l’approche ADO.NET standard pour exécuter des requêtes SQL sur IBM i avec NTi. Elle offre un contrôle total sur l’exécution et s’adapte à tous les cas: lecture de données avec ExecuteReader, écriture avec ExecuteNonQuery pour les INSERT, UPDATE et DELETE, et récupération d’une valeur unique avec ExecuteScalar pour les agrégats comme COUNT ou AVG.

SELECT

var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT REF, LIBELLE, STOCK, PRIX_UNIT FROM RETAIL.ARTICLES";
using var reader = cmd.ExecuteReader();

while (reader.Read())
{
    Console.WriteLine($"{reader.GetString(0)} | {reader.GetString(1)} | stock: {reader.GetInt32(2)} | {reader.GetDecimal(3)}");
}

SELECT paramétré

Les paramètres nommés s’ajoutent via NTiParameter. Cette approche protège contre les injections SQL et gère automatiquement les types.

var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT REF, LIBELLE, STOCK FROM RETAIL.ARTICLES WHERE STOCK < @seuil";

var param = new NTiParameter();
param.ParameterName = "@seuil";
param.Value = 50;
cmd.Parameters.Add(param);

using var reader = cmd.ExecuteReader();
while (reader.Read())
{
    Console.WriteLine($"{reader.GetString(0)} | {reader.GetString(1)} | stock: {reader.GetInt32(2)}");
}

INSERT

var cmd = conn.CreateCommand();
cmd.CommandText = "INSERT INTO RETAIL.ARTICLES (REF, LIBELLE, STOCK, PRIX_UNIT) VALUES (@ref, @libelle, @stock, @prix)";

var p1 = new NTiParameter(); p1.ParameterName = "@ref";     p1.Value = "REF-031";
var p2 = new NTiParameter(); p2.ParameterName = "@libelle"; p2.Value = "Clé USB 128Go";
var p3 = new NTiParameter(); p3.ParameterName = "@stock";   p3.Value = 200;
var p4 = new NTiParameter(); p4.ParameterName = "@prix";    p4.Value = 19.99m;

cmd.Parameters.Add(p1);
cmd.Parameters.Add(p2);
cmd.Parameters.Add(p3);
cmd.Parameters.Add(p4);
cmd.ExecuteNonQuery();

UPDATE

var cmd = conn.CreateCommand();
cmd.CommandText = "UPDATE RETAIL.ARTICLES SET STOCK = @stock WHERE REF = @ref";

var p1 = new NTiParameter(); p1.ParameterName = "@stock"; p1.Value = 999;
var p2 = new NTiParameter(); p2.ParameterName = "@ref";   p2.Value = "REF-031";

cmd.Parameters.Add(p1);
cmd.Parameters.Add(p2);
cmd.ExecuteNonQuery();

DELETE

var cmd = conn.CreateCommand();
cmd.CommandText = "DELETE FROM RETAIL.ARTICLES WHERE REF = @ref";

var p1 = new NTiParameter(); p1.ParameterName = "@ref"; p1.Value = "REF-031";
cmd.Parameters.Add(p1);
cmd.ExecuteNonQuery();

ExecuteScalar

var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT COUNT(*) FROM RETAIL.ARTICLES";
int total = (int)cmd.ExecuteScalar();
Console.WriteLine($"Nombre total d'articles : {total}");

var cmdAvg = conn.CreateCommand();
cmdAvg.CommandText = "SELECT AVG(PRIX_UNIT) FROM RETAIL.ARTICLES";
decimal prixMoyen = (decimal)cmdAvg.ExecuteScalar();
Console.WriteLine($"Prix moyen : {prixMoyen} ");

DataTable et DataSet

DataTable et DataSet permettent de charger les résultats entièrement en mémoire via NTiDataAdapter. Utile pour afficher des données dans un DataGrid, manipuler les résultats côté .NET, ou charger plusieurs tables simultanément.

DataTable

var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT REF, LIBELLE, STOCK, PRIX_UNIT FROM RETAIL.ARTICLES";

var dataTable = new DataTable();
var adapter = new NTiDataAdapter(cmd);
adapter.Fill(dataTable);

Console.WriteLine($"{dataTable.Rows.Count} lignes chargées");
foreach (DataRow row in dataTable.Rows)
    Console.WriteLine($"{row["REF"]} | {row["LIBELLE"]} | stock: {row["STOCK"]}");

DataSet

DataSet étend ce principe à plusieurs tables en mémoire en même temps. Chaque table est nommée et accessible indépendamment.

 var dataSet = new DataSet();

var cmdArticles = conn.CreateCommand();
cmdArticles.CommandText = "SELECT REF, LIBELLE, STOCK, PRIX_UNIT FROM RETAIL.ARTICLES";
new NTiDataAdapter(cmdArticles).Fill(dataSet, "ARTICLES");

var cmdClients = conn.CreateCommand();
cmdClients.CommandText = "SELECT ID_CLIENT, NOM, VILLE, PAYS FROM RETAIL.CLIENTS";
new NTiDataAdapter(cmdClients).Fill(dataSet, "CLIENTS");

Console.WriteLine($"{dataSet.Tables["ARTICLES"].Rows.Count} articles");
Console.WriteLine($"{dataSet.Tables["CLIENTS"].Rows.Count} clients");

Approche Dapper

Dapper est un micro-ORM léger développé par l’équipe Stack Overflow. Il s’intègre directement sur la connexion NTi sans aucune configuration et mappe automatiquement les résultats SQL sur vos classes C#.

Par rapport à NTiCommand, le code est plus concis, plus lisible, et élimine tout le code répétitif de lecture colonne par colonne.

A l’usage, Dapper ajoute des méthodes d’extension directement sur la connexion, Query<T> pour les SELECT et Execute pour les INSERT, UPDATE, DELETE. Les paramètres se passent via un objet anonyme.

C’est l’approche recommandée pour la plupart des projets .NET modernes qui accèdent à DB2 for i avec NTi.

Définir le modèle C#

public class Article
{
    public string REF { get; set; }
    public string LIBELLE { get; set; }
    public int STOCK { get; set; }
    public decimal PRIX_UNIT { get; set; }
}

SELECT

var articles = conn.Query<Article>(
    "SELECT REF, LIBELLE, STOCK, PRIX_UNIT FROM RETAIL.ARTICLES"
);
Console.WriteLine($"{articles.Count()} articles");

SELECT paramétré

var lowStock = conn.Query<Article>(
    "SELECT REF, LIBELLE, STOCK FROM RETAIL.ARTICLES WHERE STOCK < @seuil",
    new { seuil = 50 }
);
foreach (var a in lowStock)
    Console.WriteLine($"{a.REF} | {a.LIBELLE} | stock: {a.STOCK}");

INSERT, UPDATE, DELETE

Avec Dapper, les opérations d'écriture s'écrivent en une seule ligne. Le mapping des paramètres est automatique, et les noms des propriétés de l'objet anonyme correspondent directement aux paramètres de la requête SQL.

// INSERT
conn.Execute(
    "INSERT INTO RETAIL.ARTICLES (REF, LIBELLE, STOCK, PRIX_UNIT) VALUES (@REF, @LIBELLE, @STOCK, @PRIX_UNIT)",
    new { REF = "REF-031", LIBELLE = "Clé USB 128Go", STOCK = 200, PRIX_UNIT = 19.99m }
);

// UPDATE
conn.Execute(
    "UPDATE RETAIL.ARTICLES SET STOCK = @STOCK WHERE REF = @REF",
    new { STOCK = 999, REF = "REF-031" }
);

// DELETE
conn.Execute(
    "DELETE FROM RETAIL.ARTICLES WHERE REF = @REF",
    new { REF = "REF-031" }
);

Et maintenant ?