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 :

  • NUMERIC est le décimal zoned (étendu), DECIMAL le décimal packed (condensé) : le choix engage le format physique lu par les programmes natifs.
  • GRAPHIC / VARGRAPHIC dé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ées byte[], jamais converties en texte (sauf si la chaîne de connexion affirme un default 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 DATA

Clé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 ?

Reconnexion au serveur...

La connexion au serveur a été perdue. La page va se recharger.