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 referenceLIBELLE- VARCHAR(100) - Product labelSTOCK- INTEGER - Stock QuantityPRIX_UNIT- DECIMAL(10,2) - Unit Price
CLIENTS
ID_CLIENT- INTEGER - Customer IDNOM- VARCHAR(100) - Customer nameVILLE- VARCHAR(50) - CityPAYS- 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 ?
- Transactions - gérer les Commit et Rollback sur IBM i
- Procédure stockée - appel de procédure stockée SQL avec Dapper et DataReader
- Appeler un programme - appel de programme RPG avec paramètres d'entrée/sortie