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 provider ADO.NET natif pour IBM i. Pas de driver ODBC, pas de configuration système : un simple package NuGet suffit.
NTi est réellement asynchrone de bout en bout : ce tutoriel montre la voie async d'abord (OpenAsync, ExecuteReaderAsync, QueryAsync), chaque méthode ayant son équivalent synchrone. Les membres standard d'ADO.NET ne sont pas redocumentés ici : voir la vue d'ensemble ADO.NET chez Microsoft.
Deux approches 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) - référence produitLIBELLE- VARCHAR(100) - libellé produitSTOCK- INTEGER - quantité en stockPRIX_UNIT- DECIMAL(10,2) - prix unitaire
CLIENTS
ID_CLIENT- INTEGER - identifiant clientNOM- VARCHAR(100) - nom du clientVILLE- VARCHAR(50) - villePAYS- VARCHAR(50) - pays
É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 var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();
💡 La connexion NTi s'ouvre comme n'importe quel provider ADO.NET et fonctionne nativement avec Dapper, sans configuration supplémentaire. Pour une application web, activez le pool avec
pooling=true: voir Chaîne de connexion.
Approche ADO.NET : NTiCommand
NTiCommand implémente DbCommand : ExecuteReaderAsync pour lire, ExecuteNonQueryAsync pour écrire, ExecuteScalarAsync pour une valeur unique, et leurs équivalents synchrones ExecuteReader, ExecuteNonQuery et ExecuteScalar.
SELECT
using System;
using Aumerial.Data.Nti;
using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();
var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT REF, LIBELLE, STOCK, PRIX_UNIT FROM RETAIL.ARTICLES";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
Console.WriteLine($"{reader.GetString(0)} | {reader.GetString(1)} | stock : {reader.GetInt32(2)} | {reader.GetDecimal(3)}");
💡 Toutes les méthodes asynchrones acceptent un
CancellationToken. Par contrat, une annulation casse la connexion : l'appel lèveOperationCanceledExceptionet la connexion n'est plus utilisable.
Requêtes paramétrées : ? positionnel ou @name nommé
Les requêtes paramétrées protègent contre les injections SQL et gèrent automatiquement les types. NTi accepte deux styles de marqueurs :
?positionnel : le style natif de DB2 for i. Les paramètres sont liés dans l'ordre où ils sont ajoutés à la collection ; leur nom est ignoré.@namenommé : NTi réécrit la requête en marqueurs positionnels, en respectant les littéraux, les identifiants entre guillemets et les commentaires SQL. Chaque paramètre est retrouvé par son nom : l'ordre d'ajout est indifférent, et un même nom peut apparaître plusieurs fois dans la requête.
Marqueurs positionnels ?
using System;
using Aumerial.Data.Nti;
using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();
var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT REF, LIBELLE, STOCK FROM RETAIL.ARTICLES WHERE STOCK < ? AND PRIX_UNIT < ?";
cmd.Parameters.Add(new NTiParameter { Value = 50 }); // 1er marqueur ?
cmd.Parameters.Add(new NTiParameter { Value = 100.0m }); // 2e marqueur ?
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
Console.WriteLine($"{reader.GetString(0)} | {reader.GetString(1)} | stock : {reader.GetInt32(2)}");Marqueurs nommés @name
using System;
using Aumerial.Data.Nti;
using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();
var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT REF, LIBELLE, STOCK FROM RETAIL.ARTICLES WHERE STOCK < @seuil AND PRIX_UNIT < @prixMax";
cmd.Parameters.AddWithValue("@seuil", 50);
cmd.Parameters.AddWithValue("@prixMax", 100.0m);
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
Console.WriteLine($"{reader.GetString(0)} | {reader.GetString(1)} | stock : {reader.GetInt32(2)}");
Attention : les deux styles ne se mélangent jamais dans une même requête. NTi la rejette avec une
NTiExceptionportant le message exact :Cannot mix named (@name) and unnamed (?) parameters.
INSERT, UPDATE, DELETE
using System;
using Aumerial.Data.Nti;
using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();
// INSERT
var insert = conn.CreateCommand();
insert.CommandText = "INSERT INTO RETAIL.ARTICLES (REF, LIBELLE, STOCK, PRIX_UNIT) VALUES (@ref, @libelle, @stock, @prix)";
insert.Parameters.AddWithValue("@ref", "REF-031");
insert.Parameters.AddWithValue("@libelle", "Clé USB 128 Go");
insert.Parameters.AddWithValue("@stock", 200);
insert.Parameters.AddWithValue("@prix", 19.99m);
await insert.ExecuteNonQueryAsync();
// UPDATE
var update = conn.CreateCommand();
update.CommandText = "UPDATE RETAIL.ARTICLES SET STOCK = @stock WHERE REF = @ref";
update.Parameters.AddWithValue("@stock", 999);
update.Parameters.AddWithValue("@ref", "REF-031");
await update.ExecuteNonQueryAsync();
// DELETE
var delete = conn.CreateCommand();
delete.CommandText = "DELETE FROM RETAIL.ARTICLES WHERE REF = @ref";
delete.Parameters.AddWithValue("@ref", "REF-031");
await delete.ExecuteNonQueryAsync();ExecuteScalar
using System;
using Aumerial.Data.Nti;
using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();
var count = conn.CreateCommand();
count.CommandText = "SELECT COUNT(*) FROM RETAIL.ARTICLES";
int total = Convert.ToInt32(await count.ExecuteScalarAsync());
Console.WriteLine($"Nombre total d'articles : {total}");
var avg = conn.CreateCommand();
avg.CommandText = "SELECT AVG(PRIX_UNIT) FROM RETAIL.ARTICLES";
decimal prixMoyen = Convert.ToDecimal(await avg.ExecuteScalarAsync());
Console.WriteLine($"Prix moyen : {prixMoyen}");DataTable et DataSet
NTiDataAdapter charge les résultats entièrement en mémoire dans un DataTable ou un DataSet. Utile pour afficher des données dans un DataGrid ou charger plusieurs tables simultanément. L'API DataAdapter du framework est synchrone par nature.
DataTable
using System;
using System.Data;
using Aumerial.Data.Nti;
using var conn = new NTiConnection("server=serverName;user=userName;password=password");
conn.Open();
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.
using System;
using System.Data;
using Aumerial.Data.Nti;
using var conn = new NTiConnection("server=serverName;user=userName;password=password");
conn.Open();
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. Dapper ajoute des méthodes d'extension directement sur la connexion : QueryAsync<T> pour les SELECT (ou Query<T> en synchrone) et ExecuteAsync pour les INSERT, UPDATE, DELETE. Les paramètres se passent via un objet anonyme, et Dapper génère toujours des marqueurs nommés @name.
C'est l'approche recommandée pour la plupart des projets .NET modernes qui accèdent à DB2 for i avec NTi.
Modèle C# et SELECT
QueryAsync<Article> mappe chaque ligne sur une instance d'Article (correspondance colonne/propriété insensible à la casse), avec ou sans paramètres ; en synchrone, c'est Query<Article> :
using System;
using System.Linq;
using Aumerial.Data.Nti;
using Dapper;
using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();
// SELECT simple
var articles = (await conn.QueryAsync(
"SELECT REF, LIBELLE, STOCK, PRIX_UNIT FROM RETAIL.ARTICLES")).ToList();
Console.WriteLine($"{articles.Count} articles");
// SELECT paramétré : objet anonyme
var lowStock = await conn.QueryAsync(
"SELECT REF, LIBELLE, STOCK, PRIX_UNIT FROM RETAIL.ARTICLES WHERE STOCK < @seuil",
new { seuil = 50 });
foreach (var article in lowStock)
Console.WriteLine($"{article.REF} | {article.LIBELLE} | stock : {article.STOCK}");
public class Article
{
public string REF { get; set; }
public string LIBELLE { get; set; }
public int STOCK { get; set; }
public decimal PRIX_UNIT { get; set; }
} INSERT, UPDATE, DELETE
Avec Dapper, les opérations d'écriture s'écrivent en une seule ligne. Le mapping des paramètres est automatique : les noms des propriétés de l'objet anonyme correspondent directement aux marqueurs @name de la requête SQL.
using System;
using Aumerial.Data.Nti;
using Dapper;
using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();
// INSERT
await conn.ExecuteAsync(
"INSERT INTO RETAIL.ARTICLES (REF, LIBELLE, STOCK, PRIX_UNIT) VALUES (@REF, @LIBELLE, @STOCK, @PRIX_UNIT)",
new { REF = "REF-031", LIBELLE = "Clé USB 128 Go", STOCK = 200, PRIX_UNIT = 19.99m });
// UPDATE
await conn.ExecuteAsync(
"UPDATE RETAIL.ARTICLES SET STOCK = @STOCK WHERE REF = @REF",
new { STOCK = 999, REF = "REF-031" });
// DELETE
await conn.ExecuteAsync(
"DELETE FROM RETAIL.ARTICLES WHERE REF = @REF",
new { REF = "REF-031" });Plusieurs result sets : NextResult
Une procédure stockée DB2 for i peut retourner plusieurs result sets (clause DYNAMIC RESULT SETS). Côté .NET, on les parcourt avec NextResultAsync (ou NextResult en synchrone) sur le DataReader.
CREATE OR REPLACE PROCEDURE RETAIL.SALES_REPORT()
DYNAMIC RESULT SETS 2
LANGUAGE SQL
BEGIN
DECLARE articles CURSOR WITH RETURN FOR
SELECT REF, LIBELLE, STOCK FROM RETAIL.ARTICLES;
DECLARE clients CURSOR WITH RETURN FOR
SELECT ID_CLIENT, NOM, VILLE FROM RETAIL.CLIENTS;
OPEN articles;
OPEN clients;
END;using System;
using Aumerial.Data.Nti;
using var conn = new NTiConnection("server=serverName;user=userName;password=password");
await conn.OpenAsync();
var cmd = conn.CreateCommand();
cmd.CommandText = "CALL RETAIL.SALES_REPORT()";
using var reader = await cmd.ExecuteReaderAsync();
// 1er result set : les articles
while (await reader.ReadAsync())
Console.WriteLine($"Article : {reader.GetString(0)} ({reader.GetInt32(2)} en stock)");
// 2e result set : les clients
if (await reader.NextResultAsync())
{
while (await reader.ReadAsync())
Console.WriteLine($"Client : {reader.GetString(1)} ({reader.GetString(2)})");
}
💡 Pour les paramètres IN/OUT et l'appel de procédures stockées avec Dapper, consultez le tutoriel Procédure stockée.
Et maintenant ?
- Transactions : gérer les Commit et Rollback sur IBM i
- Procédure stockée : paramètres OUT, DataReader et Dapper
- Appeler un programme : appel de programme RPG avec paramètres d'entrée/sortie
- Chaîne de connexion : pool, MFA, TLS, timeouts