Raisonner "Objet" et développer sans langage objet ni SGBD objet

 Faire semblant de faire de l’objet…


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


Si vous trouvez que Java gère la mémoire comme une truffe                 → 

Y a le C++. 

Même que le système d’exploitation est écrit en grande partie en C++ ! 

https://www.ibm.com/docs/fr/i/7.5.0?topic=c-ile-cc-runtime-library-functions 




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 solution plus élégante :
    Table par classe; du moins conceptuellement. (une table par niveau, PK partagée). 

    • Une ligne dans PRODUIT, 

    • une dans COMPTE, 

    • une dans COMPTE_EPARGNE, 

    • une dans LIVRET_REGLEMENTE, 

    • toutes avec le même ID


  • La hiérarchie des classes se représente en table TYPE_PRODUIT qui fait référence à elle-même pour lier au code parent.


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.

  1. 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.

  2. 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.

  3. 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 : 

  • on ne fait jamais new Compte(), mais : 

    • new CompteCourant, 

    • new LivretReglemente, 

    • new CompteATerme, 

    • new CreditImmobilier, 

    • new CreditConso. 


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

Articles les plus consultés