メニューボタン
IBMi海外記事2022.11.22

SQLプリコンパイラー変数にはご用心

Ted Holt 著

有名な ヘニー・ヤングマンのジョークで、患者がこう尋ねます。「先生、こうすると痛みが出るのですが」 医者は答えます。「だとしたら、そうするのはやめておくようにしてください」 古くさいジョークはさておき、そうすると痛みが出るというようなプログラミング作法の正体を突き止める作業に、私は何十年もの時間を費やしてきました。そして、正体を突き止めたら、そうするのはやめておくようにしています。その代表例と言えるのが、RPGプログラム内でのSQLプリコンパイラーが定義する変数(SQLCODE、SQLSTATE、およびSQLER3など)の誤った使い方です。

「これらの変数の何が問題なのですか」という声が聞こえてきそうです。それらの変数はグローバルであり、グローバルな変数は厄介なものです。 グローバル変数には不意打ちを食らうことがあります。思いも寄らぬときに、値が変わるのです。グローバル変数のせいで、デバッグ作業に何時間も費やす羽目になり、もっと楽しいことに充てられたはずの時間が奪われたこともありました。数週間、数か月間、または数年間、何の問題もなく動いていたプログラムが、グローバル変数のせいで、突然、おかしくなるのを目の当たりしたこともあります。

これが、プリコンパイラー変数の取り扱いには細心の注意を払う必要がある、ただ1つの理由です。しかし、信頼性の高いプログラムをお望みなら、理由は1つあれば十分です。

以下に示す例を見て、どこが問題なのか確かめてみましょう。そうした問題がどれほど簡単に回避できるのかを目にしたら、驚かれると思います。

以下は、複数行フェッチを使用して、物理ファイル内のすべてのレコードを処理するプログラムです(QCUSTCDTファイルは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;

以下は、プログラムの呼び出しからの出力です。

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

出力行が12行あることに注意しておいてください。

このソフトウェア エンジニアリングの模範例のようなコードは、数か月間、本番稼働していました。その後、Junior J. Programmer氏は、未払残高が25ドル以上になった顧客の、顧客アカウント番号と残高を別の表へ書き込むプログラムを書くようにと指示を受けました。以下が、Junior氏が書き込む表です。

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

モダンなプログラマーであるJunior氏は、ループ内にINSERTコマンドを追加します。

**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;

Junior氏による修正は正しく機能しません。

  • 「Johnson」のデータがCREDITDUE表に入らない。
  • Junior氏の修正で既存のレポートが破損した。
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

出力行は10行しかありません。「Henning」と「Johnson」のデータはどうなってしまったのでしょうか。

ループはSQLER3変数に条件付けられており、変数にはフェッチされた行数が格納されています。では、それぞれのFETCHの後、およびそれぞれの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;                

以下が出力です。

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         

未払残高が100ドルだった「Doe」の後のSQLER3の値に注目してください。INSERTがSQLER3の値を「1」へ変更しています。挿入されたのが1行のみだったからです。 RPGコンパイラーは、ループ実行時にFORコマンドの最終値が変更されるのを許容します。ここで起こったのがそれです。 「1」という更新された最終値に到達すると、ループは止まります。そして、プログラムは次のFETCHを続行しました。

解決策は、ループをSQLER3変数に条件付けるのを止めることです。

dcl-s   CountFetchedRows   uns(5);

   CountFetchedRows = SQLER3;
   for ndx = 1 to CountFetchedRows;

もちろん、SQLER3を回避するためにGET DIAGNOSTICSを使用することもできます。

dcl-s   CountFetchedRows   uns(5);

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

これも良いでしょう。そして、間違っているとは言いませんが、私はこのようにGET DIAGNOSTICSを使用することはありません。すでにメモリー内にある値を取得するためにプログラムを呼び出す理由が見当たらないからです。 ちょうど、これから出席しようとしているイベントでソフト ドリンクが出されることが分かっているのに、わざわざソフト ドリンクを買うようなものです。

私は、様々なショップで作業をしてきましたが、SQLCODE(またはSQLCOD)変数をテストするループを数多く見てきました。 以下は、非常によく目にするものです。

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

やはりこれも、間違っているとは言いませんが、細心の注意を払う必要があります。そのようなループが、意図した挙動にならないケースがよくありました。私なら、上のサンプル プログラムで使用した「DOW *ON」の手法を使います。

こうしたSQLプリコンパイラー変数が、非常に多くのバグの原因であることが分かったため、可能な限り、それらを使用しないようにしています。 SQLプリコンパイラー変数を使用するときには、次の2つのルールを決めています(今のところ2つです)。

  • プリコンパイラー変数の値を使用する必要があるときは、すぐに使用する。プログラム内でその値が後で必要になる場合は、すぐにその値を自分で用意した変数に保管する。
  • ループの実行をプリコンパイラー変数に条件付けるときは、細心の注意を払う。

リストに加えていただけるとしたら幸いです。

あわせて読みたい記事

PAGE TOP