Mapping et types DB2 for i
Le provider mappe les types .NET vers les types DB2 for i avec des défauts qui fonctionnent contre n'importe quel schéma existant. Cette page couvre les leviers spécifiques IBM i : le stockage Unicode, l'épinglage d'un type de stockage exact (la clé de la cohabitation avec les programmes RPG et COBOL), le scaffolding des bases legacy, les clés HiLo et la concurrence optimiste ROW CHANGE TIMESTAMP.
Les mécanismes eux-mêmes (annotations, API fluent, conventions) sont ceux d'EF Core standard : voir la documentation Microsoft. Ici, seul le rendu DB2 for i est détaillé.
Texte et Unicode
Par défaut, une propriété string devient une colonne VARCHAR (longueur donnée par HasMaxLength, sinon par l'option VarcharMaxLength). L'annotation standard [Unicode] (ou IsUnicode() en fluent) stocke la colonne en Unicode, dans le CCSID configuré par l'option UnicodeCcsid (défaut 1208, UTF-8) :
using Microsoft.EntityFrameworkCore;
public class Customer
{
public int Id { get; set; }
[Unicode] // colonne Unicode (CCSID 1208 par défaut)
public string Name { get; set; } = "";
[Unicode(false)] // jamais Unicode, même si ForceUnicode() est actif
public string Code { get; set; } = "";
}
Ou en fluent, dans le contexte :
using Microsoft.EntityFrameworkCore;
public class CustomerContext : DbContext
{
public DbSet Customers => Set();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.Property(c => c.Name)
.IsUnicode();
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseNTi("server=MYIBMI;user=MYUSER;password=MYPASSWORD;database=MYLIB;");
}
L'option ForceUnicode() de UseNTi bascule tout le modèle en Unicode ; [Unicode(false)] en exclut alors une propriété. Voir Introduction et configuration pour les options du provider.
L'écriture est stricte : une valeur non représentable dans le CCSID de la colonne cible est rejetée avec une erreur explicite, jamais substituée ni tronquée en silence.
Épingler un type exact : HasColumnType
Quand une table doit rester consommable par des programmes RPG ou COBOL existants, ou coller à un fichier physique en place, épinglez le type de stockage exact avec [Column(TypeName = ...)] ou HasColumnType(...). Deux points propres à DB2 for i :
NUMERICest le décimal zoned (étendu),DECIMALle décimal packed (condensé) : le choix engage le format physique lu par les programmes natifs.GRAPHIC/VARGRAPHICdéclarent des colonnes DBCS ;TIMESTAMP(n)accepte une précision de 0 à 12.
using System;
using System.ComponentModel.DataAnnotations.Schema;
public class InvoiceLine
{
public int Id { get; set; }
[Column(TypeName = "NUMERIC(7,2)")] // zoned : le format attendu par le programme RPG existant
public decimal Amount { get; set; }
[Column(TypeName = "DECIMAL(11,0)")] // packed
public decimal AccountNumber { get; set; }
[Column(TypeName = "VARGRAPHIC(50)")] // DBCS
public string Label { get; set; } = "";
[Column(TypeName = "TIMESTAMP(12)")] // précision de 0 à 12
public DateTime UpdatedAt { get; set; }
}
L'équivalent fluent est Property(e => e.Amount).HasColumnType("NUMERIC(7,2)"). Sans HasColumnType ni HasPrecision, les decimal reçoivent la précision et l'échelle configurées par l'option DecimalDefaults.
Scaffolding d'une base existante (legacy)
Le reverse engineering fonctionne avec l'outillage standard (une passe de dotnet ef dbcontext scaffold) :
dotnet ef dbcontext scaffold "server=MYIBMI;user=MYUSER;password=MYPASSWORD;database=MYLIB;" Aumerial.EntityFrameworkCore --output-dir Models
Le scaffolding couvre tables, vues, index, clés étrangères, commentaires et séquences, et sait lire les artefacts hérités :
- les décimaux zoned (
NUMERIC) et packed (DECIMAL), restitués avec leur type de stockage exact pour que les migrations ultérieures ne les altèrent pas ; - les colonnes
FOR BIT DATA(CCSID 65535), binaires par nature : mappéesbyte[], jamais converties en texte (sauf si la chaîne de connexion affirme undefault ccsid) ; - les colonnes
DECFLOAT; - les tables sans clé primaire, générées keyless (
HasNoKey) et interrogeables en lecture ; - les vues.
// extrait typique d'une entité scaffoldée depuis un fichier physique legacy
[Column(TypeName = "NUMERIC(5,0)")]
public decimal Qty { get; set; }
public byte[] LegacyData { get; set; } = null!; // CHAR(16) FOR BIT DATAClés HiLo
UseHiLo génère les clés côté client à partir d'une séquence DB2 for i (NEXT VALUE FOR), par réservation de blocs : aucun aller-retour d'identité par insertion. C'est le mécanisme standard d'EF Core (séquences), adossé ici à une séquence DB2 for i créée par les migrations.
using Microsoft.EntityFrameworkCore;
public class Order
{
public int Id { get; set; }
public string Customer { get; set; } = "";
}
public class OrderContext : DbContext
{
public DbSet Orders => Set();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.Property(o => o.Id)
.UseHiLo("ORDERS_HILO"); // séquence créée et gérée par les migrations
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseNTi("server=MYIBMI;user=MYUSER;password=MYPASSWORD;database=MYLIB;");
}
modelBuilder.UseHiLo() applique le mécanisme à tout le modèle.
Concurrence optimiste : IsRowChangeTimestamp
IsRowChangeTimestamp() déclare un jeton de concurrence adossé à la colonne native ROW CHANGE TIMESTAMP, réécrite par le serveur à chaque UPDATE : c'est l'analogue DB2 for i du rowversion de SQL Server. En cas de modification concurrente, SaveChangesAsync lève la DbUpdateConcurrencyException standard.
using System;
using Microsoft.EntityFrameworkCore;
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public DateTime RowVersion { get; set; }
}
public class CatalogContext : DbContext
{
public DbSet Products => Set();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.Property(p => p.RowVersion)
.IsRowChangeTimestamp();
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseNTi("server=MYIBMI;user=MYUSER;password=MYPASSWORD;database=MYLIB;");
}
Après un UPDATE, rechargez l'entité avant de la sauvegarder à nouveau depuis le même contexte : le serveur régénère le jeton à chaque mise à jour, la valeur suivie par le contexte est donc périmée dès la sauvegarde.
using Microsoft.EntityFrameworkCore;
await using var db = new CatalogContext();
var product = await db.Products.FirstAsync(p => p.Id == 1);
product.Name = "Nouveau nom";
await db.SaveChangesAsync();
await db.Entry(product).ReloadAsync(); // récupère le nouveau ROW CHANGE TIMESTAMP
product.Name = "Nom corrigé";
await db.SaveChangesAsync();Et maintenant ?
- Introduction et configuration : versions, UseNTi, options du provider
- Migrations : Migrate, scripts idempotents, limites DB2 for i
- CRUD avec EF Core 8 : exemple complet avec Entity Framework Core sur DB2 for i