
Si on veut faire de la programmation objet sur IBM i, y a Java.
Si, si, Java est implémenté.
C’est le produit 5770JV1 : https://www.ibm.com/support/pages/java-ibm-i
Malheureusement, RPG Free Form n’est pas un langage objet.
Pas d’affolement, on peut raisonner conceptuellement “objet”, et avoir une implémentation pas si compliquée que ça qui ne va pas faire exploser les “Powers of 10”.
On va prendre un exemple pas trop compliqué, des produits bancaires :
Produit num contrat, client, ouverture, clôture, devise, agence ├── Compte IBAN, solde, date dernier relevé │ ├── Compte_Courant découvert autorisé, taux agios, chéquier │ └── Compte_Epargne taux nominal, plafond, intérêts acquis │ ├── Livret_Reglemente code réglementaire, exonération fiscale │ └── Compte_A_Terme échéance, pénalité de retrait anticipé └── Credit capital, taux, durée, 1re échéance ├── Credit_Immobilier bien financé, type de garantie └── Credit_Conso TAEG, objet du financement |
Cette arborescence montre bien les objets et leurs héritages divers.
Manque de chance, il n’y a pas d’extension objet dans DB2 for i non plus.
Pas de langage objet, pas d’extension dans le SGBD, on est mal !
Mais on va essayer de s’en sortir sans devenir fou dans notre tête et en restant assez clair.
C’est pas vraiment du K.I.S.S. au niveau de la conception car le sujet est complexe. Dans mon sujet précédent, je fustigeais les complications inutiles, je ne recommandais pas de les fuir. Et vous allez voir plus tard qu'on y gagne en développement... gniark, gniark, gniark... 😈
La mauvaise solution : On met tout dans une table. Ben, oui, c’est sympa, y a pas de jointures, on a tout sous la papatte 😺.
Mais c’est moche, très moche, y a des “NULL” partout. Bref, vous l’aurez compris et ça me chatouille les moustaches de l’écrire : C’EST NUL !!!

La table racine PRODUIT rassemble les infos communes à tous les objets (ou tables, (ou entités))
Chaque niveau suivant a pour clé primaire la clé étrangère vers son parent immédiat et non pas vers la racine. C'est ce détail qui matérialise le chemin d'héritage dans le schéma général.
COMPTE → PRODUIT
COMPTE_EPARGNE → COMPTE
Et la traduction en DDL :
CREATE TABLE TYPE_PRODUIT ( COD_TYPE CHAR(4) NOT NULL PRIMARY KEY, COD_PARENT CHAR(4) REFERENCES TYPE_PRODUIT (COD_TYPE), LIB_TYPE VARCHAR(50) NOT NULL, IND_ABSTRAIT CHAR(1) NOT NULL DEFAULT 'N' ); -- PROD(abstrait) / CPTE(abstrait,PROD) / EPAR(abstrait,CPTE) / LIVR(EPAR) …
Contenu de la table TYPE_PRODUIT
COD_TYPE COD_PARENT LIB_TYPE NIVEAU CHEMIN PROD (null) Produit 0 PROD CPTE PROD Compte 1 PROD/CPTE EPAR CPTE Compte épargne 2 PROD/CPTE/EPAR LIVR EPAR Livret réglementé 3 PROD/CPTE/EPAR/LIVR CATE EPAR Compte à terme 3 PROD/CPTE/EPAR/CATE CRED PROD Crédit 1 PROD/CRED IMMO CRED Crédit immobilier 2 PROD/CRED/IMMO _______________________________________________________________________________
CREATE TABLE PRODUIT ( ID_PRODUIT BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, COD_TYPE CHAR(4) NOT NULL REFERENCES TYPE_PRODUIT (COD_TYPE), NUM_CONTRAT VARCHAR(20) NOT NULL UNIQUE, ID_CLIENT BIGINT NOT NULL REFERENCES CLIENT (ID_CLIENT), DAT_OUVERT DATE NOT NULL, DAT_CLOTURE DATE, COD_DEVISE CHAR(3) NOT NULL DEFAULT 'EUR', COD_AGENCE CHAR(5) NOT NULL ); _______________________________________________________________________________
CREATE TABLE COMPTE ( ID_PRODUIT BIGINT NOT NULL PRIMARY KEY, IBAN CHAR(27) NOT NULL UNIQUE, MNT_SOLDE DECIMAL(15,2) NOT NULL DEFAULT 0, DAT_RELEVE DATE, CONSTRAINT FK_CPTE FOREIGN KEY (ID_PRODUIT) REFERENCES PRODUIT (ID_PRODUIT) ON DELETE CASCADE ); _______________________________________________________________________________
CREATE TABLE COMPTE_EPARGNE ( ID_PRODUIT BIGINT NOT NULL PRIMARY KEY, TAUX_NOMINAL DECIMAL(5,3) NOT NULL, MNT_PLAFOND DECIMAL(15,2), MNT_INTERETS DECIMAL(15,2) NOT NULL DEFAULT 0, CONSTRAINT FK_EPAR FOREIGN KEY (ID_PRODUIT) REFERENCES COMPTE (ID_PRODUIT) ON DELETE CASCADE ); _______________________________________________________________________________
CREATE TABLE LIVRET_REGLEMENTE ( ID_PRODUIT BIGINT NOT NULL PRIMARY KEY, COD_REGLEM CHAR(6) NOT NULL, -- LEPA, LDDS, CEL... IND_EXONERE CHAR(1) NOT NULL DEFAULT 'O', CONSTRAINT FK_LIVR FOREIGN KEY (ID_PRODUIT) REFERENCES COMPTE_EPARGNE (ID_PRODUIT) ON DELETE CASCADE ); |
Les vues complètent la BdD en donnant l’accès immédiat à toutes les données des objets ou entités “feuilles”.
Livret Réglementé
Compte à Terme
Crédit Immobilier
Crédit Conso
CREATE VIEW V_LIVRET_REGLEMENTE AS SELECT P.ID_PRODUIT, P.NUM_CONTRAT, P.ID_CLIENT, P.DAT_OUVERT, P.COD_AGENCE, C.IBAN, C.MNT_SOLDE, E.TAUX_NOMINAL, E.MNT_PLAFOND, E.MNT_INTERETS, L.COD_REGLEM, L.IND_EXONERE FROM PRODUIT P JOIN COMPTE C ON C.ID_PRODUIT = P.ID_PRODUIT JOIN COMPTE_EPARGNE E ON E.ID_PRODUIT = P.ID_PRODUIT JOIN LIVRET_REGLEMENTE L ON L.ID_PRODUIT = P.ID_PRODUIT; _______________________________________________________________________________
CREATE VIEW V_COMPTE_A_TERME AS SELECT P.ID_PRODUIT, P.NUM_CONTRAT, P.ID_CLIENT, P.DAT_OUVERT, P.COD_AGENCE, C.IBAN, C.MNT_SOLDE, E.TAUX_NOMINAL, E.MNT_PLAFOND, E.MNT_INTERETS, A.DAT_ECHEANCE, A.MNT_PENALITE FROM PRODUIT P JOIN COMPTE C ON C.ID_PRODUIT = P.ID_PRODUIT JOIN COMPTE_EPARGNE E ON E.ID_PRODUIT = P.ID_PRODUIT JOIN COMPTE_A_TERME A ON A.ID_PRODUIT = P.ID_PRODUIT; _______________________________________________________________________________
CREATE VIEW V_CREDIT_IMMOBILIER AS SELECT P.ID_PRODUIT, P.NUM_CONTRAT, P.ID_CLIENT, P.DAT_OUVERT, P.COD_AGENCE, C.MNT_CAPITAL, C.TAUX, C.DUREE_MOIS, C.DAT_1ERE_ECHEANCE, I.BIEN_FINANCE, I.TYP_GARANTIE FROM PRODUIT P JOIN CREDIT C ON C.ID_PRODUIT = P.ID_PRODUIT JOIN CREDIT_IMMOBILIER I ON I.ID_PRODUIT = P.ID_PRODUIT; _______________________________________________________________________________
CREATE VIEW V_CREDIT_CONSO AS SELECT P.ID_PRODUIT, P.NUM_CONTRAT, P.ID_CLIENT, P.DAT_OUVERT, P.COD_AGENCE, C.MNT_CAPITAL, C.TAUX, C.DUREE_MOIS, C.DAT_1ERE_ECHEANCE, O.TAEG, O.OBJ_FINANCEMENT FROM PRODUIT P JOIN CREDIT C ON C.ID_PRODUIT = P.ID_PRODUIT JOIN CREDIT_CONSO O ON O.ID_PRODUIT = P.ID_PRODUIT; |
V_HIERARCHIE_TYPE dit, pour chaque type de produit, où il se situe dans l'arbre des Objets/Entités.
Sa profondeur et sa lignée complète sans que l’on ait à coder cette hiérarchie en dur quelque part.
Elle lit le seul endroit qui fait foi, la table TYPE_PRODUIT, et en déduit l'arborescence.
Deux colonnes calculées, deux usages.
NIVEAU définit la profondeur.
Elle sert à l'affichage (indenter un arbre de sélection de produit dans une appli, trier du général au particulier) et à filtrer : « donne-moi tous les types feuilles » ou « tous les types abstraits d'un certain étage ».
CHEMIN définit la lignée matérialisée. Exemple avec “/PROD/CPTE/EPAR/LIVR” prêt à être filtré par LIKE.
“PROD” comme “Produit”.
“CPTE” comme “Compte”.
“EPAR” comme “Epargne”.
“LIVR” comme “Livret”.
etc…
C'est la colonne la plus utile, pour trois raisons.
Filtrer une sous-arborescence en une passe.
WHERE CHEMIN LIKE 'PROD/CPTE/%' donne tous les types de comptes, toutes profondeurs confondues, sans récursivité côté appelant.
« Tout ce qui est un compte » devient un simple LIKE.
Répondre à « X est-il un descendant de Y ? » sans remonter l'arbre à la main.
La question « ce produit est-il une sorte de compte ? » se lit directement dans le chemin.
Générer les vues concrètes. C'est le point : “/PROD/CPTE/EPAR/CATE” qui est exactement la liste ordonnée des tables à joindre pour fabriquer la vue V_COMPTE_A_TERME.
CREATE VIEW V_HIERARCHIE_TYPE AS WITH ARBRE (COD_TYPE, COD_PARENT, LIB_TYPE, NIVEAU, CHEMIN) AS ( SELECT COD_TYPE, COD_PARENT, LIB_TYPE, 0, CAST(COD_TYPE AS VARCHAR(200)) FROM TYPE_PRODUIT WHERE COD_PARENT IS NULL UNION ALL SELECT T.COD_TYPE, T.COD_PARENT, T.LIB_TYPE, A.NIVEAU + 1, A.CHEMIN CONCAT '/' CONCAT T.COD_TYPE FROM TYPE_PRODUIT T JOIN ARBRE A ON T.COD_PARENT = A.COD_TYPE ) SELECT * FROM ARBRE; – oui, je sais les Select *, c’est mal, mais le bien le mal… _______________________________________________________________________________
CREATE VIEW V_CTRL_ABSTRAIT AS SELECT P.ID_PRODUIT, P.NUM_CONTRAT, P.COD_TYPE FROM PRODUIT P JOIN TYPE_PRODUIT T ON T.COD_TYPE = P.COD_TYPE WHERE T.IND_ABSTRAIT = 'O';
|
Et si on veut pousser le bouchon un peu loin (Maurice !)

On peut introduire un contrôle d’objet avec la vue V_CTRL_ABSTRAIT
Cette vue est un détecteur d'anomalie : cette vue liste les produits qui n'ont pas le droit d'exister.
En temps normal, elle ne renvoie rien. Mais comme un bug est si vite arrivé... ou un "Insert" sauvage...
Certains types ne sont là que pour être dérivés, jamais instanciés.
Produit, Compte, Compte épargne, Crédit sont abstraits : on ne détient pas “un compte“ dans l'absolu, on détient un compte courant, un livret, un compte à terme.
Seules les feuilles (CompteCourant, LivretReglemente, CompteATerme, CreditImmobilier, CreditConso) sont concrètes, réellement instanciables.
C'est exactement la notion de classe abstraite en objet :
CREATE VIEW V_CTRL_ABSTRAIT AS SELECT P.ID_PRODUIT, P.NUM_CONTRAT, P.COD_TYPE FROM PRODUIT P JOIN TYPE_PRODUIT T ON T.COD_TYPE = P.COD_TYPE WHERE T.IND_ABSTRAIT = 'O'; |
En d’autres termes, une requête sur cette vue “donne tout produit dont le type est marqué abstrait”.
Voici pour la base de données !
Et pour les traitements, on fait des fonctions dans des Programmes de Service.
Un SRVPGM par table, donc un SRVPGM par classe.
SRV_PRODUIT sait lire/écrire la ligne racine et rien d'autre.
Exemple concret : Comment créer un compte à terme ?
La branche du compte à terme, c'est PRODUIT → COMPTE → COMPTE_EPARGNE → COMPTE_A_TERME :
4 fonction creer() dans 4 SRVPGM
D'abord, une structure de données qui transporte tout ce qu'on veut créer. Chaque étage y puise ce qui le concerne et ignore le reste :
// Copybook partagé — QCPYSRC,PRODUIT_H dcl-ds t_produit qualified template; // niveau PRODUIT num_contrat char(20); id_client int(20); dat_ouvert date; cod_agence char(5); // niveau COMPTE iban char(27); mnt_solde packed(15:2); // niveau COMPTE_EPARGNE taux_nominal packed(5:3); mnt_plafond packed(15:2); // niveau COMPTE_A_TERME dat_echeance date; mnt_penalite packed(15:2); end-ds; |
La racine : SRV_PRODUIT. Le seul étage sans parent. C'est lui qui fabrique l'ID_PRODUIT et le rend :
dcl-proc SRV_PRODUIT_creer export; dcl-pi *n int(20); p likeds(t_produit) const; end-pi; dcl-s w_id int(20);
exec sql INSERT INTO PRODUIT (COD_TYPE, NUM_CONTRAT, ID_CLIENT, DAT_OUVERT, COD_AGENCE) VALUES ('CATE', :p.num_contrat, :p.id_client, :p.dat_ouvert, :p.cod_agence);
exec sql VALUES IDENTITY_VAL_LOCAL() INTO :w_id; // récupère l'id qu'on vient de générer return w_id; end-proc;
|
Étage COMPTE. Il appelle son parent d'abord, récupère l'id, insère sa ligne, rend l'id :
dcl-proc SRV_COMPTE_creer export; dcl-pi *n int(20); p likeds(t_produit) const; end-pi; dcl-s w_id int(20);
w_id = SRV_PRODUIT_creer(p); // super() : la ligne PRODUIT existe après ça
exec sql INSERT INTO COMPTE (ID_PRODUIT, IBAN, MNT_SOLDE) VALUES (:w_id, :p.iban, :p.mnt_solde);
return w_id; end-proc;
|
Étage COMPTE_EPARGNE. Exactement le même motif, un cran plus haut :
dcl-proc SRV_EPARGNE_creer export; dcl-pi *n int(20); p likeds(t_produit) const; end-pi; dcl-s w_id int(20);
w_id = SRV_COMPTE_creer(p); // super()
exec sql INSERT INTO COMPTE_EPARGNE (ID_PRODUIT, TAUX_NOMINAL, MNT_PLAFOND) VALUES (:w_id, :p.taux_nominal, :p.mnt_plafond);
return w_id; end-proc; |
La feuille — SRV_CATE. Le point d'entrée que l'application appelle. Même forme, il ferme la chaîne :
dcl-proc SRV_CATE_creer export; dcl-pi *n int(20); p likeds(t_produit) const; end-pi; dcl-s w_id int(20);
w_id = SRV_EPARGNE_creer(p); // super() : PRODUIT, COMPTE, EPARGNE sont posés
exec sql INSERT INTO COMPTE_A_TERME (ID_PRODUIT, DAT_ECHEANCE, MNT_PENALITE) VALUES (:w_id, :p.dat_echeance, :p.mnt_penalite);
return w_id; end-proc; |
L'appelant. Côté application, on ne voit qu'un seul appel.
Toute la cascade est invisible et c'est tout l'intérêt :
dcl-s id_nouveau int(20); dcl-ds mon_cat likeds(t_produit);
mon_cat.num_contrat = 'CAT-2026-00042'; mon_cat.id_client = 1087; mon_cat.dat_ouvert = %date(); mon_cat.cod_agence = '75019'; mon_cat.iban = 'FR7630004000031234567890143'; mon_cat.mnt_solde = 15000.00; mon_cat.taux_nominal = 3.250; mon_cat.mnt_plafond = 0; // pas de plafond sur un CAT mon_cat.dat_echeance = %date() + %years(3); mon_cat.mnt_penalite = 150.00;
id_nouveau = SRV_CATE_creer(mon_cat); // un seul appel, quatre INSERT dessous |
A l'usage, c'est assez KISS, n'est-ce pas ? 😺
Ce qui se passe dans l'ordre :
SRV_CATE → SRV_EPARGNE → SRV_COMPTE → SRV_PRODUIT qui insère la ligne PRODUIT et génère l'id.
Puis ça remonte, COMPTE s'insère, COMPTE_EPARGNE s'insère, COMPTE_A_TERME s'insère.
Quatre lignes, un id partagé, et l'appelant n'a rien vu de la mécanique.
Attention : ces quatre INSERT doivent réussir ou échouer ensemble, donc à encadrer d'une transaction (COMMIT/ROLLBACK) une fois le squelette en place, sinon un plantage au 3ᵉ étage laisse un produit à moitié créé.
Pour les plus taquins d’entre vous, voici ce que donnerait la même chose avec du Python Objet :
import ibm_db_dbi
conn = ibm_db_dbi.connect() # connexion locale à la base du système
## ______________________________________________________________________________
class Produit: """La racine — équivalent de SRV_PRODUIT. Le seul niveau sans parent."""
cod_type = 'PROD' # redéfini par chaque sous-classe
def __init__(self, conn, num_contrat, id_client, dat_ouvert, cod_agence): self.conn = conn self.id_produit = None # sera rempli à la création self.num_contrat = num_contrat self.id_client = id_client self.dat_ouvert = dat_ouvert self.cod_agence = cod_agence
def creer(self): cur = self.conn.cursor() cur.execute( "INSERT INTO PRODUIT " "(COD_TYPE, NUM_CONTRAT, ID_CLIENT, DAT_OUVERT, COD_AGENCE) " "VALUES (?, ?, ?, ?, ?)", (self.cod_type, self.num_contrat, self.id_client, self.dat_ouvert, self.cod_agence)) cur.execute("VALUES IDENTITY_VAL_LOCAL()") self.id_produit = cur.fetchone()[0] # l'id généré, stocké sur l'objet return self.id_produit
## ______________________________________________________________________________
class Compte(Produit): """Équivalent de SRV_COMPTE."""
cod_type = 'CPTE'
def __init__(self, iban, mnt_solde, **kwargs): super().__init__(**kwargs) # le parent range SA part des données self.iban = iban self.mnt_solde = mnt_solde
def creer(self): super().creer() # ← super() : la ligne PRODUIT existe après ça cur = self.conn.cursor() cur.execute( "INSERT INTO COMPTE (ID_PRODUIT, IBAN, MNT_SOLDE) VALUES (?, ?, ?)", (self.id_produit, self.iban, self.mnt_solde)) return self.id_produit
## ______________________________________________________________________________
class CompteEpargne(Compte): """Équivalent de SRV_EPARGNE."""
cod_type = 'EPAR'
def __init__(self, taux_nominal, mnt_plafond, **kwargs): super().__init__(**kwargs) self.taux_nominal = taux_nominal self.mnt_plafond = mnt_plafond
def creer(self): super().creer() # PRODUIT puis COMPTE sont posés cur = self.conn.cursor() cur.execute( "INSERT INTO COMPTE_EPARGNE (ID_PRODUIT, TAUX_NOMINAL, MNT_PLAFOND) " "VALUES (?, ?, ?)", (self.id_produit, self.taux_nominal, self.mnt_plafond)) return self.id_produit
## ______________________________________________________________________________
class CompteATerme(CompteEpargne): """La feuille — équivalent de SRV_CATE."""
cod_type = 'CATE'
def __init__(self, dat_echeance, mnt_penalite, **kwargs): super().__init__(**kwargs) self.dat_echeance = dat_echeance self.mnt_penalite = mnt_penalite
def creer(self): super().creer() # PRODUIT, COMPTE, EPARGNE tous posés cur = self.conn.cursor() cur.execute( "INSERT INTO COMPTE_A_TERME (ID_PRODUIT, DAT_ECHEANCE, MNT_PENALITE) " "VALUES (?, ?, ?)", (self.id_produit, self.dat_echeance, self.mnt_penalite)) return self.id_produit
## ______________________________________________________________________________
from datetime import date
cat = CompteATerme( # niveau PRODUIT conn=conn, num_contrat='CAT-2026-00042', id_client=1087, dat_ouvert=date.today(), cod_agence='75019', # niveau COMPTE iban='FR7630004000031234567890143', mnt_solde=15000.00, # niveau COMPTE_EPARGNE taux_nominal=3.250, mnt_plafond=0, # niveau COMPTE_A_TERME dat_echeance=date(2029, 7, 28), mnt_penalite=150.00, )
id_nouveau = cat.creer() # un seul appel, quatre INSERT dessous conn.commit()
|
Le super() de Python fait tout seul ce que la chaîne de SRVPGM fait à la main.
Dans SRV_CATE, on écrit explicitement SRV_EPARGNE_creer(p).
Ici on écrit super().creer(), et c'est Python qui sait, par la déclaration class CompteATerme(CompteEpargne), que le parent est CompteEpargne.
Le graphe d'héritage est déclaré une fois (CompteEpargne), pas câblé à chaque appel.
C'est toute la différence entre un langage objet et la simulation avec le RPG Free Form : en Free Form le lien enfant → parent est dans le corps du code (un appel en dur), chez Python il est dans la signature de la classe.
L'id circule différemment, et c'est plus propre.
En RPG on fait remonter l'id par la valeur de retour, d'étage en étage.
En Python, comme tout est sur self, la racine écrit self.id_produit une fois et les trois niveaux au-dessus le relisent directement.
Pas de passage de paramètre, pas de retour à chaîner.
C'est l'état partagé de l'objet qui l'offre, exactement ce que self apporte et qu'une structure passée de proc en proc n'a pas nativement.
L'ordre d'exécution est identique au Free Form, parce que super().creer() est la première instruction : on descend jusqu'à Produit.creer() qui insère et génère l'id, puis on remonte en insérant COMPTE, COMPTE_EPARGNE, COMPTE_A_TERME.
Même descente, même remontée, mêmes contraintes de FK respectées dans le bon sens.
Commentaires
Enregistrer un commentaire