RPGで制約違反を処理する
著者まえがき: この記事の内容は、最初に「Handling Constraint Violations in RPG」および「Handling Constraints Revisited」という2つの別々の記事として掲載されたものです。記事の内容は、フリーフォームRPG用に、また、2009年以降にRPGに導入された、いくつかのコーディングの機能強化に合わせるべく、アップデートを行っています
制約は、ずいぶん前から存在していましたが、すべてのプログラマーのツール キットの中に入り込んでいたわけではないことは明らかなようです。このことについては、既存のアプリケーションに制約を実装するのには慎重を要する場合が多いという点では納得できるところはありますが、制約が新規のアプリケーションで幅広く使用されない理由の説明にはなりません。
また、ついでに言えば、データ ウェアハウジングなどで、データをiから他のデータベース サーバー(SQL ServerやOracleなど)へ移動/コピーするべき理由の1つとして、データベースで制約がないことを挙げているのを耳にしたことがあります。「純粋なデータベース人間」にとっては、制約がないことは、データベースでないことを意味します。
RPGプログラムで制約違反をどのように処理したらよいかということが、制約をアプリケーションに組み込むことに対するハードルの1つとなっているわけですが、この記事がそうしたハードルを取り除くのに少しでも役立ってくれればと思います。
まずは、制約について見直してみることから始めましょう。
制約とは何か
制約は、設定した規則に基づいて、表(ファイル)間でデータ値の論理的一貫性が保たれていること、およびデータの関係性が妥当であることがデータベース マネージャーによって保証される参照整合性の機能です。
たいそう立派なもののように聞こえるかもしれませんが、これは皆さんがすでに行っていることです。ただし、アプリケーション プログラム内で、ではありますが。たとえば、インボイス ファイルに従属しているインボイスがある場合はその顧客を削除することはできない、あるいは、年齢が16歳未満なら雇用してはならない、といったものです。そのようなルールは、RPGプログラムではロジックを通じて適用されています。
しかし、アプリケーションが拡張され、データが従来のRPGアプリケーションの外部からアクセスできるようになったら、どうなるでしょうか。これらのルールがすべてのインターフェース全体で一貫していることが極めて重要になります。そうしたルールを実装する方法として、データベース マネージャーを通じて実装されるよりも、もっと良い方法があるとしたらどのようなものなのでしょうか。
制約を処理するための数多くのコマンド(ADDPFCST、CHGPFCST、RMVPFCST、WRKPFCST、EDTCPCST、DSPCPCST)がありますが、制約を処理する最も簡単な方法は、間違いなく、SQLを通じての方法でしょう。まずは、次のように定義されたDEPARTMENT表とEMPLOYEE表を見てみましょう。
CREATE OR REPLACE TABLE DEPARTMENT (
DEPARTMENT_CODE FOR DEPTNO CHAR(3) NOT NULL DEFAULT ,
DEPARTMENT_NAME FOR DEPTNAME VARCHAR(36) NOT NULL DEFAULT )
RCDFMT DEPARTR ;
CREATE OR REPLACE TABLE EMPLOYEE (
EMPLOYEE_ID FOR EMPID CHAR(6) NOT NULL DEFAULT ,
FIRSTNAME VARCHAR(25) NOT NULL DEFAULT ,
LASTNAME VARCHAR(25) NOT NULL DEFAULT ,
DEPARTMENT_CODE FOR DEPTNO CHAR(3) NOT NULL DEFAULT ,
HIRE_DATE DATE NOT NULL DEFAULT,
BIRTH_DATE DATE NOT NULL DEFAULT,
SALARY DECIMAL(9, 2) NOT NULL DEFAULT )
RCDFMT EMPLOYEER ;
キー制約では、表に対して1つの主キーと複数のユニーク キーを定義できます。最終結果はアクセス経路ですが、対応する論理ファイルはありません。簡単に言ってしまえば、主キー制約は、表の第1正規形とも言えるでしょうか。キー制約を定義することは、外部キー制約のための要件にもなります。次のように、DEPARTMENT表およびEMPLOYEE表に対して、主キー制約を定義することができます。
ALTER TABLE DEPARTMENT
ADD CONSTRAINT PK_DEPARTMENT
PRIMARY KEY (DEPARTMENT_CODE) ;
ALTER TABLE EMPLOYEE
ADD CONSTRAINT PK_EMPLOYEE
PRIMARY KEY (EMPLOYEE_ID) ;
外部キー制約は、従属表と親表の2つの表の間で定義されます。親ファイルには、主キー制約が定義されている必要があります。
以下に示す外部キー制約では、EMPLOYEE表とDEPARTMENT表の間に依存関係が設定されます。依存関係とは、ここで言えば、EMPLOYEE表に従属行がある場合は、DEPARTMENT表の行を削除できないというようなものです。ここでは、2つの表にそれぞれあるDEPARTMENT_CODE列間に依存関係があるということになります(結合の場合と同様です)。
ALTER TABLE EMPLOYEE
ADD CONSTRAINT FK_EMPLOYEE_TO_DEPARTMENT_ERR0001
FOREIGN KEY( DEPARTMENT_CODE )
REFERENCES DEPARTMENT ( DEPARTMENT_CODE )
ON DELETE RESTRICT
ON UPDATE RESTRICT ;
チェック制約では、表内の1つ以上の列に対してデータの妥当性検査を定義することができます。チェック制約は、行に置かれるデータに対するルールとして使用されるSQL WHERE節と似ていると言えます。次のチェック制約では、生年月日と雇用年月日との間の年数の差が16以上である(すなわち16歳未満は雇用しない)ことが保証されます。
ALTER TABLE EMPLOYEE
ADD CONSTRAINT C_HIRE_AGE_ERR0002
CHECK( YEAR(HIRE_DATE-BIRTH_DATE) >= 16 ) ;
すべての制約の名前がロング ネームであることにお気付きでしょうか。制約を作成しても、対応するシステム オブジェクトが作成されるわけではないため、10文字の名前であることは求められません。
制約違反を処理する
制約違反をどのように処理するかは、ネイティブI/Oを使用しているか、組み込みSQLを使用しているかで異なります。まずはネイティブI/Oから見て行きましょう。
RPGプログラムでは、制約違反をどのようにしてチェックしているでしょうか。データベース違反をチェックするのと同じ方法、すなわち、ファイル命令でのI/Oエラーをトラップすることによってです。もちろん、このやり方の問題点は、制約違反があったことを伝えるだけだということです。定義されている無数の制約のうち、どの制約が問題の原因となったのかを教えてくれるわけではありません。
次のコード片は、EMPLOYEE表にレコードを書き込みます。I/Oエラーが検出されると、u_send_fileError()サブプロシージャーが呼び出され、Employee Details表のステータス コードが渡されます。sendFileError()サブプロシージャーは、エラーを「処理した」場合は*offを返し、そうでない場合は*onを返します。
write(E) employeeR;
if %error;
if u_send_fileError(%status(employee));
exsr *PSSR;
endIF;
return *On;
endIf;
ファイル エラーをチェックする
u_send_fileError()サブプロシージャーは、「認識されている」ステータス コードのいずれかに対応するアプリケーション メッセージを送信します。「認識されている」ステータス コードのどれでもない(Otherの処理)場合は、標準メッセージが送信され、プロシージャーは*On状態を返します。u_add_Message()サブプロシージャーについては、「メッセージを取得する、パート1」という記事で説明しています。
顕著な違いは、制約エラー(ステータス1022または1222)に対してルーチンが行う処理です。つまり、エラー メッセージを送信するのではなく、サブプロシージャーSendConstraintMsg()を呼び出します。
dcl-C STAT_DUPLICATE 01021;
dcl-C STAT_CONSTRAINT_1 01022;
dcl-C STAT_CONSTRAINT_2 01222;
dcl-C STAT_TRIGGER_1 01023;
dcl-C STAT_TRIGGER_2 01024;
dcl-C ERR_NOTFOUND 'ALL9001';
dcl-C ERR_CHANGED 'ALL9002';
dcl-C ERR_DUPLICATE 'ALL9003';
dcl-C ERR_CONSTRAINT 'ALL9004';
dcl-C ERR_TRIGGER 'ALL9005';
dcl-C ERR_UNKNOWN 'ALL9006';
dcl-C ERR_NOT_NUMBER 'ALL9007';
dcl-C ERR_NOT_DATE 'ALL9008';
dcl-Proc u_send_FileError export;
dcl-Pi *n ind;
status int(5) const;
end-Pi;
// Duplicate
if (status = STAT_DUPLICATE);
u_add_Message(ERR_DUPLICATE);
// Referential constraint
elseIf (status = STAT_constRAINT_1 or
status = STAT_constRAINT_2);
send_constraintMsg();
// Trigger
elseIf (status = STAT_TRIGGER_1 or
status = STAT_TRIGGER_2);
u_add_Message(ERR_TRIGGER);
// Other
else;
u_add_Message(ERR_UNKNOWN);
return *On;
endIf;
return *Off;
end-Proc;
名前の付け方など
SendConstraintMsg()サブプロシージャーについて説明する前に、制約の名前の付け方、およびどのようにしたら制約に対して意味が分かりやすいメッセージを関連付けられるかについて少し考えてみましょう。3つの選択肢があります。
- 送信したいメッセージに制約の名前を関連付けるロジックをプログラム内に用意する(あまり実用的な選択肢ではない)。
- 制約名をメッセージ テキストとして送信する。10文字の命名規則に制限されないため、たとえば「この人は雇用するには若過ぎる」という制約名も有効。
- アプリケーションでエラー メッセージに対して独自のメッセージ ファイルを使用するというやり方に慣れている場合は、制約名の一部にエラー メッセージIDを含める、という命名規則を採用するというのはどうでしょうか。私としてはこの方法がお勧めであり、以下で説明するのもこの方法です。
どのようにして制約とメッセージを関連付けるようにするにせよ、制約名を取得する手法については、この後、SendConstraintMsg()が行う処理についての説明を通じて見て行くこととします。
以前に定義した外部キー制約とチェック制約についてもう一度見ておきましょう。制約名の一部としてどのような形でメッセージIDが含められているか(ERR0001およびERR0002)に注目してください。要は、プログラムが制約違反を受け取ったら、制約名を取得し、メッセージIDを抽出し、そのメッセージを使用してユーザーに意味の分かるエラーを提示するということです。
この制約の名前は何でしょう
ファイルI/Oエラーのメッセージそのものでは、制約の識別はできないかもしれませんが、そこには利用可能な情報があります。I/Oエラーを受け取ったプログラムのプログラム メッセージ待ち行列をチェックして、制約違反メッセージ(CPF502D、CPF502E、CPF502F、CPF503A、CPF503B)がないか探してみてください。メッセージの2次レベル メッセージ テキストに、制約名が記されているのです。したがって、SendConstraintMsg()は、プログラム メッセージ待ち行列全体を検索し、必要な制約メッセージを取得し、制約名を抽出する必要があるということになります。
プログラム メッセージ待ち行列全体を読み取るのに、QMHRCVPM APIが使用されます。QMHRCVPMを呼び出すためのプロトタイプのパラメーターを以下に示します。
- msgInfoは、取得した情報が格納されるデータ構造です。msgInfoLenは、msgInfoで使用されるデータ構造の長さです。
- formatNameは、データが返される際に使用されるフォーマットです。この例ではRCVM0100が使用されています。
- callStackとcallStackCtrは組み合わせて使用されます。callStackは'*'にセットされ、callStackCtrは2にセットされます。これは、呼び出しスタックで2つ上のメッセージ待ち行列からメッセージを取得したい、ということを示しています。sendConstraintMsg()が、呼び出しスタックの現行エントリーです(callStack = '*')。SendFileError()は、呼び出しスタックで1つ上です。I/Oエラーを受け取ったプロシージャーが、呼び出しスタックで2つ上です(callStackCtr = 2)。
- msgType、msgKey、およびwaitTimeは、待ち行列からどのメッセージを読み取るか、および待ち行列でメッセージが到着するのをどれくらい待機するかを示します。このルーチンは、待ち行列の最後から先頭へ読み取りを行います。
- msgActionは、メッセージを読み取ったときに待ち行列上のメッセージで行うアクションです。このルーチンは、それを削除します。
- errorForAPIは、標準APIエラー構造です。
dcl-Pr receive_Msg extPgm('QMHRCVPM');
msgInfo char(3000) options(*VarSize);
msgInfoLen int(10) const;
formatName char(8) const;
callStack char(10) const;
callStackCtr int(10) const;
msgType char(10) const;
msgKey char(4) const;
waitTime int(10) const;
msgAction char(10) const;
errorForAPI like(APIError);
end-Pr;
以下は、SendConstraintMsg()サブプロシージャーのデータ定義部分です。QMHRCVPM APIから返されたデータは、このmsgBackデータ構造に格納されます。このルーチンは、主にmsgIdおよびmsgDataフィールドに着目します。msgIdは、探している制約エラー メッセージを識別するのに使用され、msgDataには177桁目から始まる制約名を格納します。
dcl-Proc send_constraintMsg;
dcl-Pi *n end-Pi;
// DS returned by QMHRCVPM for format RCVM0100
dcl-Ds msgBack qualified inz;
byteReturned int(10);
byteAvail int(10);
msgSeverity int(10);
msgId char(7);
msgType char(2);
msgKey char(4);
*n char(7);
CCSIDInd int(10);
CCSIDReplace int(10);
lengthreturn int(10);
lengthAvail int(10);
msgData char(1024);
end-Ds;
dcl-S setMsgKey char(4);
dcl-S prevMsgKey like(setMsgKey);
dcl-S constraint char(50);
dcl-S msgId char(7);
sendConstraintMsg()サブプロシージャーの残りの部分は以下の通りです。
setMsgKey = *ALLx'00';
doW setMsgKey <> prevMsgKey;
prevMsgKey = setMsgKey;
receive_Msg( msgBack
: %size(msgBack)
: 'RCVM0100'
: '*'
: 2
: '*PRV'
: setMsgKey
: 0
: '*SAME'
: APIError);
if (msgBack.msgId = 'CPF502D' Or
msgBack.msgId = 'CPF502E' Or
msgBack.msgId = 'CPF502F' Or
msgBack.msgId = 'CPF503A' Or
msgBack.msgId = 'CPF503B');
constraint = %subst(msgBack.msgData: 177);
monitor;
msgId = %subSt(constraint: %scan(' ':constraint)-7);
u_add_Message(msgId);
return;
on-Error;
u_add_Message(ERR_CONSTRAINT);
return;
endMon;
endIf;
setMsgKey = msgBack.msgKey;
endDo;
u_add_Message(ERR_CONSTRAINT);
return;
end-Proc;
このルーチンは以下のように動作します。
- ループにより、呼び出しスタックで2つ上のプログラム メッセージ待ち行列からメッセージを読み取ります。
- 待ち行列の各メッセージのデータが、RCVM0100フォーマットでmsgBackデータ構造に入れられます。
- そのメッセージが制約メッセージの1つである場合、制約名がメッセージ データ(177桁目から始まる)から取り出され、必要なエラー メッセージIDは、制約名の最後の7桁目から取り出されます。
- エラー メッセージの送信で問題がある場合、またはプログラム メッセージ待ち行列に制約メッセージがない場合は、汎用メッセージが送信されます。
- メッセージが送信されると、プロシージャーは終了します。/li>
ネイティブI/Oの場合に、意味が分かりやすい制約名を取得する手法はこのようなものでした。しかし、組み込みSQLではこの手法はうまく行きません。
組み込みSQLの問題
上で説明した手法の基本前提は、RPGプログラムが制約違反を受け取ると、呼び出し元プログラムのプログラム メッセージ待ち行列の検索を行うプロシージャーを呼び出して、違反の原因となった制約の名前を含む関連メッセージを見つける、というものです。
ここで問題が生じます。
RPGプログラム内でSQLを直接コーディングしたとしても、SQLステートメントを実行するのは、このプログラムではありません。RPGプログラムで以下をコーディングしたとすると、
exec SQL
insert into employee (
employee_ID, firstname, lastname, department_code,
hire_date, birth_date)
values(
:employee_ID, :firstname, :lastname, :department_code,
:hire_date, :birth_date);
SQLプリコンパイラーによって次のように変換されます。
//*exec SQL
//* insert into employee (
//* employee_ID, firstname, lastname, department_code,
//* hire_date, birth_date)
//* values(
//* :employee_ID, :firstname, :lastname, :department_code,
//* :hire_date, :birth_date);
SQL_00005 = EMPLOYEE_ID;
SQL_00006 = FIRSTNAME;
SQL_00007 = LASTNAME;
SQL_00008 = DEPARTMENT_CODE;
SQL_00009 = HIRE_DATE;
SQL_00010 = BIRTH_DATE;
SQLER6 = -4;
SQLROUTE_CALL(
SQLCA
: SQL_00000
);
そのため、insertは、プログラムQSYS/QSQROUTE、あるいはQSYS/QSQROUTEによって呼び出されるプログラムまたはプロシージャーによって実行されます(上のコードで、SQLROUTEはQSYS/QSQROUTEの名前付き定数です)。
この時点で、サブプロシージャーを呼び出してRPGプログラムのプログラム メッセージ待ち行列を検索することは意味がありません。当該メッセージがあるのは、QSYS/QSQROUTE(または他のプログラムまたはプロシージャー)のプログラム メッセージ待ち行列であるからです。
解決策
組み込みSQLステートメントの場合、制約違反の原因となった制約の名前を判別するのは、RPGで経なければならなかったプロセスに比べると、はるかに簡単だと言えます。SQLは簡単に制約の名前を教えてくれます。
SQL連絡域(SQL Communications Area)(SQLプリコンパイラーによってプログラムに置かれるデータ構造SQLCA)のSQLERRMCフィールドに、制約の名前が格納されるからです(制約違反がある場合)。
注意が必要な点が1つあります。SQLERRMCは70文字のフィールドとして定義されていますが、制約名は可変長フィールドです。SQLERRMCの最初の2文字は、制約名の長さを指定しています。そのため、次のようなデータ構造を定義するようにするとよいかもしれません。
dcl-ds getName len(70);
constraint varchar(68);
end-ds;
そして、SQLERRMCをgetNameへコピーすることによって、制約名が得られることになります。
上の例の場合、次のようにコーディングしていたとすれば、
exec SQL
insert into employee (
employee_ID, firstname, lastname, department_code,
hire_date, birth_date)
values(
:employee_ID, :firstname, :lastname, :department_code,
:hire_date, :birth_date);
if (SQLSTATE = '23513');
getName = SQLERRMC;
endIf;
制約違反を受け取ると同時に、constraintフィールドの値が、FK_EMPLOYEE_TO_DEPARTMENT_ERR0001またはC_HIRE_AGE_ERR0002になっていたことでしょう。
SQL連絡域の内容はSQLステートメントが実行されるたびにリセットされるため、エラーの原因となったSQLステートメントの直後に名前を取得するようにしてください。
終わりに
制約は、アプリケーションの内と外の両方でのデータインテグリティを確保する手段を提供するために、アプリケーションで使用するべき強力なツールであると言えるでしょう。
それだけにとどまらず、どれだけ多くのコードをプログラムから取り出してデータベースに置くことができるようになるかという視点からも、考えてみるとよいと思います。
制約をプログラムで処理するのは、最初はややこしそうだと思えるかもしれませんが、処理はすべて、いくつかのサブプロシージャーに置くことができ、見えなくすることができます。唯一、厄介なのは、ファイル命令でのI/Oエラーをトラップするプロセスです。
IBM Championにして『Re-engineering RPG Legacy Applications』の著者であるPaul Tuohy氏は、IBMミッドレンジ界におけるアプリケーションのモダナイゼーションおよび開発テクノロジーの分野で非常に著名なコンサルタント兼トレーナーです。現在は、アイルランドのダブリンのコンサルタント会社、ComCon社のCEOを務める傍ら、「System i Developer」コンソーシアムの運営にもパートナーとして参画しています。Susan Gantner氏およびJon Paris氏とともに、年2回、RPG & DB2 Summit を主催しています。