Guru : Méfiez-vous des variables du précompilateur SQL

26 septembre 2022

Ted Holt

Dans une célèbre blague de Henny Youngman, un patient dit : « Docteur, ça fait mal quand je fais ça », ce à quoi le médecin répond : « Alors ne faites pas ça. Blagues à part, j’ai passé des décennies à essayer d’identifier les pratiques de programmation qui blessent quand je les fais, et après les avoir identifiées, j’ai cessé de les faire. Un cas concret est l’utilisation abusive des variables que le précompilateur SQL définit dans mes programmes RPG, des variables telles que SQLCODE, SQLSTATE et SQLER3.

“Alors,” je vous entends demander, “est le problème avec ces variables ? « Eh bien, elles sont globales, et les variables globales sont mauvaises. Les variables globales sont sournoises et changeront leur valeur lorsque vous vous y attendrez le moins. Grâce aux variables globales, j’ai passé des heures à déboguer alors que je préférais faire quelque chose de plus agréable. J’ai vu des programmes fonctionner pendant des semaines, des mois ou des années sans problème et se détraquer soudainement à cause d’une variable globale.

Ce n’est qu’une raison pour être prudent avec les variables du précompilateur, mais si vous aimez les programmes fiables, une raison suffit.

Regardons un exemple et voyons ce qui peut mal tourner. Vous serez peut-être surpris de voir avec quelle facilité ces problèmes sont évités.

Voici un programme qui utilise une extraction multi-lignes pour traiter tous les enregistrements d’un fichier physique. (Au cas où vous ne le sauriez pas, le fichier QCUSTCDT se trouve dans la bibliothèque QIWS.)

**free
ctl-opt  actgrp(*new) option(*srcstmt);

dcl-f qsysprt printer(132);

dcl-c   cSQLEOF            '02000';

dcl-ds  CustomerInfo
           extname('QCUSTCDT') dim(5) qualified inz  end-ds;

dcl-s   MaxRows            uns(5)    inz(%elem(CustomerInfo));
dcl-s   ndx                uns(5);

*inlr = *on;

exec sql  declare c1 cursor for select * from qcustcdt order by lstnam;

exec sql  open c1;
if SQLSTATE > cSQLEOF;
   snd-msg *escape ('Open failed, state=" + SQLState);
endif;

dow *on;

   exec sql  fetch c1 for :MaxRows rows into :CustomerInfo;

   if SQLState > cSQLEOF;
      snd-msg *escape ("Fetch failed, state=" + SQLState);
   endif;

   if SQLState = cSQLEOF;
      leave;
   endif;

   for ndx = 1 to SQLER3;
      writeln (%editc(CustomerInfo(ndx).CUSNUM: "4') + ' ' +
                      CustomerInfo(ndx).LSTNAM       + ' ' +
               %editc(CustomerInfo(ndx).CDTDUE: 'L'));
   endfor;

enddo;

exec sql  close c1;
return;

dcl-proc writeln;
   dcl-pi *n;
      inString   varchar(132)   const;
      inPosition uns(3)         const   options(*nopass);
   end-pi;

   dcl-ds   ReportLine   len(132)   end-ds;
   dcl-s    Position     uns(3);

   if %parms() >= %ParmNum(inPosition);
      Position = inPosition;
   else;
      Position = 1;
   endif;

   %subst(ReportLine: Position) = inString;
   write qsysprt ReportLine;

end-proc writeln;

Voici la sortie de l’appel du programme.

583990 Abraham      .00
846283 Alison       .00
475938 Doe       100.00
938472 Henning      .00
938485 Johnson    33.50
839283 Jones        .00
192837 Lee          .50
389572 Stevens     1.50
693829 Thomas       .00
397267 Tyron        .00
392859 Vine         .00
593029 Williams     .00

Notez qu’il y a 12 lignes de sortie.

Après que ce spécimen stellaire de génie logiciel soit en production depuis quelques mois, Junior J. Programmer est invité à faire en sorte que le programme écrive le numéro de compte client et le crédit dû à une autre table lorsque le montant du crédit dû est d’au moins 25 dollars. Voici la table à laquelle Junior doit écrire.

create table CreditDue as
   (select cusnum, cdtdue from qiws.qcustcdt)
definition only

Junior, étant un programmeur moderne, ajoute une commande INSERT à l’intérieur de la boucle.

**free

ctl-opt  actgrp(*new) option(*srcstmt);

dcl-f qsysprt printer(132);

dcl-c   cSQLEOF            '02000';

dcl-ds  CustomerInfo
           extname('QCUSTCDT') dim(5) qualified inz  end-ds;

dcl-s   MaxRows            uns(5)    inz(%elem(CustomerInfo));
dcl-s   CountFetchedRows   uns(5);
dcl-s   ndx                uns(5);

dcl-s   XCUSNUM    zoned(6);
dcl-s   XCDTDUE    packed(9:2);

exec sql set option commit=*none;

*inlr = *on;

exec sql  declare c1 cursor for select * from qcustcdt order by lstnam;

exec sql  open c1;
if SQLSTATE > cSQLEOF;
   snd-msg *escape ('Open failed, state=" + SQLState);
endif;

dow *on;

   exec sql  fetch c1 for :MaxRows rows into :CustomerInfo;

   if SQLState > cSQLEOF;
      snd-msg *escape ("Fetch failed, state=" + SQLState);
   endif;

   if SQLState = cSQLEOF;
      leave;
   endif;

   for ndx = 1 to SQLER3;
      writeln (%editc(CustomerInfo(ndx).CUSNUM: "4') + ' ' +
                      CustomerInfo(ndx).LSTNAM       + ' ' +
               %editc(CustomerInfo(ndx).CDTDUE: 'L'));
      XCUSNUM = CustomerInfo(ndx).CUSNUM;
      XCDTDUE = CustomerInfo(ndx).CDTDUE;
      if XCDTDUE >= 25.00;
         exec sql  insert into CreditDue values (:XCUSNUM, :XCDTDUE);
         if SQLState > cSQLEOF;
            snd-msg *escape ('Insert failed, state=" + SQLState);
         endif;
      endif;
   endfor;

enddo;

exec sql  close c1;

return;

dcl-proc writeln;
   dcl-pi *n;
      inString   varchar(132)   const;
      inPosition uns(3)         const   options(*nopass);
   end-pi;

   dcl-ds   ReportLine   len(132)   end-ds;
   dcl-s    Position     uns(3);

   if %parms() >= %ParmNum(inPosition);
      Position = inPosition;
   else;
      Position = 1;
   endif;

   %subst(ReportLine: Position) = inString;
   write qsysprt ReportLine;

end-proc writeln;

La modification de Junior ne fonctionne pas correctement.

  • Les données de Johnson n’entrent pas dans la table CREDITDUE.
  • Junior a cassé le rapport existant.
583990 Abraham      .00
846283 Alison       .00
475938 Doe       100.00
839283 Jones        .00
192837 Lee          .50
389572 Stevens     1.50
693829 Thomas       .00
397267 Tyron        .00
392859 Vine         .00
593029 Williams     .00

Il n’y a que 10 lignes de sortie. Qu’est-il arrivé à Henning et Johnson ?

La boucle est conditionnée à la variable SQLER3, qui contient le nombre de lignes extraites, regardons donc la valeur après chaque FETCH et après chaque INSERT.

   writeln ("FETCH --> SQLER3 = ' + %char(SQLER3));         
   for ndx = 1 to SQLER3;                                   
      writeln (%editc(CustomerInfo(ndx).CUSNUM: '4') + ' ' +
                      CustomerInfo(ndx).LSTNAM       + ' ' +
               %editc(CustomerInfo(ndx).CDTDUE: 'L'));      
      XCUSNUM = CustomerInfo(ndx).CUSNUM;
      XCDTDUE = CustomerInfo(ndx).CDTDUE;
      if XCDTDUE >= 25.00;
         exec sql  insert into CreditDue values (:XCUSNUM, :XCDTDUE);
         if SQLState > cSQLEOF;
            snd-msg *escape ('Insert failed, state=" + SQLState);
         endif;
      endif;
      writeln ("--> SQLER3 = ' + %char(SQLER3));            
   endfor;                                                  

Voici la sortie.

FETCH --> SQLER3 = 5    
583990 Abraham      .00 
--> SQLER3 = 5          
846283 Alison       .00 
--> SQLER3 = 5          
475938 Doe       100.00 
--> SQLER3 = 1          
FETCH --> SQLER3 = 5    
839283 Jones        .00 
--> SQLER3 = 5          
192837 Lee          .50 
--> SQLER3 = 5          
389572 Stevens     1.50 
--> SQLER3 = 5          
693829 Thomas       .00 
--> SQLER3 = 5          
397267 Tyron        .00 
--> SQLER3 = 5          
FETCH --> SQLER3 = 2    
392859 Vine         .00
--> SQLER3 = 2         
593029 Williams     .00
--> SQLER3 = 2         

Notez la valeur de SQLER3 après Doe, qui avait un solde créditeur de 100 $. L’INSERT a changé la valeur de SQLER3 en 1, puisqu’une seule ligne a été insérée. Le compilateur RPG permet de changer la valeur terminale de la commande FOR lors de l’exécution de la boucle, et c’est ce qui s’est passé ici. Ayant atteint la valeur terminale mise à jour de 1, la boucle s’est arrêtée et le programme a continué avec le FETCH suivant.

La solution consiste à arrêter de conditionner la boucle à la variable SQLER3.

dcl-s   CountFetchedRows   uns(5);

   CountFetchedRows = SQLER3;
   for ndx = 1 to CountFetchedRows;

Bien sûr, vous pouvez également utiliser GET DIAGNOSTICS pour éviter SQLER3.

dcl-s   CountFetchedRows   uns(5);

   exec sql  get diagnostics :CountFetchedRows = Row_Count;
   for ndx = 1 to CountFetchedRows;

C’est bien, et je ne dirai pas que c’est faux, mais je n’utilise pas GET DIAGNOSTICS de cette manière car je ne vois aucune raison d’appeler un programme pour récupérer une valeur qui est déjà en mémoire. C’est comme acheter une boisson gazeuse lorsque l’événement auquel vous assistez en fournit.

J’ai vu une multitude de boucles qui testent la variable SQLCODE (ou SQLCOD) dans divers ateliers où j’ai travaillé. En voici une très courante.

EXEC SQL FETCH . . .
DOW SQLCOD <> 100;
   . . . more stuff . . .
   EXEC SQL FETCH . . .
ENDDO

Encore une fois, je ne dirai pas que c’est faux, mais soyez très prudent. J’ai vu de telles boucles échouer à produire le comportement souhaité. Je préfère la méthode DOW *ON que j’ai utilisée dans les exemples de programmes ci-dessus.

J’ai tracé tellement de bogues à ces variables de précompilation SQL, que j’ai finalement décidé de les éviter autant que possible. J’ai développé quelques règles empiriques (jusqu’à présent).

  • Lorsque vous avez besoin d’utiliser la valeur d’une variable de précompilation, utilisez-la immédiatement. Si vous avez besoin de cette valeur plus tard dans le programme, enregistrez immédiatement la valeur dans une variable qui vous est propre.
  • Soyez très prudent lorsque vous conditionnez l’exécution d’une boucle à une variable de précompilation.

Si vous pouvez ajouter à la liste, je serai très reconnaissant.

HISTOIRES CONNEXES

Les variables globales sont mauvaises (Phil Koopman)

Henny Youngman Docteur Blagues

Leave a Comment

Your email address will not be published. Required fields are marked *