トリガーというのはRDB の一般的な機能のひとつで、データベーステーブルに UPDATE/INSERT/DELETE などの更新があった時に自動的に事前定義しておいたスクリプトを発行することができる、という機能です。
発行されるタイミングは更新の前(Before)/後(After)が指定でき、更新前であれば何らかのチェック、更新後であればロギングなどの用途に使われることが多いようです。(ちなみに Microsoft SQL Server には Before トリガーはありません。After のみです。また、SQL Server には、Oracle にも DB2/400 にも存在する行レベルトリガーとステートメントレベルトリガーの区別もありません。そんなふうにデータベースエンジンによって多少異なっています)
通常、処理はSQL で記述されます。Oracle の場合はPL/SQL、SQL Server の場合はTransact-SQL、DB2/400
の場合はDB2 ファミリーのSQL PL(Procedure Language) といった具合です。ちなみに
SQL PL はANSI で定められたSQL/PSM (Persistent Stored Modules) とほぼ同じものになります。
SQL トリガーについては、このサイトでも「SQL トリガーの作成」や「SQL トリガーの起動タイミング (Each Row/Each Statement)」、「SQL トリガーのモード (DB2SQL/DB2ROW)」、「INSTEAD OF トリガーとカラムの暗号化」などで取り上げてきました。
DB2/400 には SQL で記述されるトリガーの他に、RPG や COBOL、C などで書いたプログラムをトリガー処理プログラムとして登録できる、という機能があります。つまり、SQL では記述しにくい細かな処理や、それどころか SQL では記述しようのないデータベース関連以外の処理なども、トリガーによって起動される処理として定義することができます。
また、もうひとつさらに、これは他のデータベースでは見たことがないのですが、毎 Row の Read が行われたタイミングでトリガーを起動させることができます。つまり READ トリガー、ということになります。
これは行単位に何を読み取ったのかがわかりますから監査目的にも使用できますし、ある特別な行だけをある特定の人にしか見せないといったようなセキュリティ目的にも使用できます。
CPYF だろうが DSPPFM だろうが、レコード内容を読み取った時点でトリガー処理が行われます。ですから、原理的に毎行に対して行われる
AFTER トリガーということになりますね。
ちなみに、「データベースのオープン毎に処理を行う (Exit プログラム)」で紹介した方法では、SQL や Query には反応しますが DSPPFM には反応しません。
ただし、原理的に、毎行実行されてしまいますので、ブロッキングや非同期バッファリングなど、OS が読み込みのパフォーマンスを上げるために行っている処理の大半が効かなくなってしまいます。パフォーマンスの問題が生じますので、考慮が必要です。たとえば、プログラム内で使用する場合、バッチで大量の読み込みが発生するような場合は、後述する CHGPFTRG コマンドで一時的に無効にしてから実行する、などの工夫が必要になるでしょう。
プログラムを指定するトリガーは ADDPFTRG コマンドで追加します。SQL の ALTER TABLE コマンドでは行えません。

トリガー名は*GEN のままなので、システムが生成したトリガー名がついています。

トリガーを有効にしたり無効にしたりすることが、CHGPFTRG コマンドでできます。登録されているトリガー単位でできるのですが、その場合はトリガー名を指定することになります。トリガー名を*GEN でシステムに生成させるとかなり使いにくい名前になるので、覚えやすい名前にしてちゃんと名前を指定しておいた方がいいでしょう。

RMVPFTRG コマンドでテーブルとトリガーの結びつきを開放することができます。トリガープログラムが削除されたりしてしまうわけではありません。

先ほど書いたように、トリガー名を指定することもできます。ファイル/テーブル内で固有であればOK です。

DSPFD コマンドでファイル記述を見てみると、ちゃんと指定された名前になっていますね。

トリガー名を指定して、個別にトリガーの状態を変更させることができます。
こういう時にはやはり *GEN ではなく、名前をつけておいた方がなにかと便利です。
いったん無効にして、

大量のレコード/行の挿入などトリガーが存在するとパフォーマンスに多大な影響のあるような処理を行って、終了後に戻す、といったようなことが割合一般的に行われています。

トリガー処理プログラムには、トリガーバッファとそのバッファの長さが渡されます。
D Trg_Simple PR
D TrgBuffer LIKEDS(TrgInfo)
D TrgBufferLen 10i 0
*
D Trg_Simple PI
D TrgBuffer LIKEDS(TrgInfo)
D TrgBufferLen 10i 0
トリガーバッファの内容は以下のようなものです。
D TrgInfo DS Qualified
D File 10a
D Library 10a
D Member 10a
D Event 1a
D Time 1a
D CmtLckLvl 1a
D 3
D CCSID 10i 0
D RRN 10i 0
D 4
D BfrRecOfs 10i 0
D BfrRecLen 10i 0
D BfrNullOfs 10i 0
D BfrNullLen 10i 0
D AftRecOfs 10i 0
D AftRecLen 10i 0
D AftNullOfs 10i 0
D AftNullLen 10i 0
トリガーの起動イベントの種類とタイミングによりますが、更新前/更新後のレコードのイメージを取得することができます。
実際に QCUSTCDT テーブルを使用するのではなくコピーした別のファイルを使うのですが、レコード様式が一緒なのでそのまま
ExtName(QCUSTCDT) と指定してしまっています。コンパイルする時にこのファイルがライブラリーリスト上にないとエラーになります。コンパイル時には、ライブラリー
QIWS をライブラリーリストに入れておいてください。ちなみにコンパイルに特別な考慮点はありません。ごく普通にして
OK です。
D BfrRecPtr S *
D Original E DS ExtName(QCUSTCDT)
D Based(BfrRecPtr)
D Qualified
*
D AftRecPtr S *
D New E DS ExtName(QCUSTCDT)
D Based(AftRecPtr)
D Qualified
*
/Free
BfrRecPtr = %addr(TrgBuffer) + TrgBuffer.BfrRecOfs;
AftRecPtr = %addr(TrgBuffer) + TrgBuffer.AftRecOfs;
以下のプログラムでは、トリガーバッファ内から READ されたファイルの名前とそのレコードの RRN とを、ジョブの実行ユーザーをプログラム状況データ構造から拾って、あわせて表示するようにしています。
H DFTACTGRP(*no)
*
D SDS
D dsUser 254 263a
D crUser 358 367a
*
D Trg_Simple PR
D TrgBuffer LIKEDS(TrgInfo)
D TrgBufferLen 10i 0
*
D Trg_Simple PI
D TrgBuffer LIKEDS(TrgInfo)
D TrgBufferLen 10i 0
*
D TrgInfo DS Qualified
D File 10a
D Library 10a
D Member 10a
D Event 1a
D Time 1a
D CmtLckLvl 1a
D 3
D CCSID 10i 0
D RRN 10i 0
D 4
D BfrRecOfs 10i 0
D BfrRecLen 10i 0
D BfrNullOfs 10i 0
D BfrNullLen 10i 0
D AftRecOfs 10i 0
D AftRecLen 10i 0
D AftNullOfs 10i 0
D AftNullLen 10i 0
*
D BfrRecPtr S *
D Original E DS ExtName(QCUSTCDT)
D Based(BfrRecPtr)
D Qualified
*
D AftRecPtr S *
D New E DS ExtName(QCUSTCDT)
D Based(AftRecPtr)
D Qualified
*
D Insert C '1'
D Delete C '2'
D Update C '3'
D Read C '4'
*
D After C '1'
D Before C '2'
*
D none C '0'
D change C '1'
D cs C '2'
D all C '3'
*
/Free
BfrRecPtr = %addr(TrgBuffer) + TrgBuffer.BfrRecOfs;
AftRecPtr = %addr(TrgBuffer) + TrgBuffer.AftRecOfs;
dsply ('USRPRF ' +
%trim(dsUser) + ' saw: ');
dsply ('USRPRF ' +
%trim(crUser) + ' saw: ');
dsply ( %trim(TrgBuffer.Library) + '/' +
%trim(TrgBuffer.File) + '(' +
%trim(TrgBuffer.Member) + '):' +
'RRN=' +
%trim(%char(TrgBuffer.RRN)) );
dsply (%trim(%char(Original.CUSNUM)) + ' ' +
Original.LSTNAM +
' ' + Original.STATE);
// *inLR = *on ; //
return ;
/End-Free
|
では、どういうふうに動くのか、確認してみましょう。
DSPPFM コマンドで、トリガーが設定されているテーブルの内容を表示させてみます。

こんなかんじのメッセージが毎行をアクセスするために表示されていきます。
DSPPFM コマンドの処理結果が画面に表示されるより前に、各レコードは事前に一件一件アクセスされているわけですからね。

最後にまとめて表示結果が返ってきます。

実行中にジョブの呼び出しスタックを見てみると、ちゃんとトリガープログラムがスタックに乗っているのが確認できます。

「SQL スクリプトの実行」経由でも実行してみましょう。

DSPLY 命令の表示結果は QZDASOINIT ジョブから、デフォルトで QSYSOPR に送られます。
ちなみに、メッセージの表示順は下から上になっています。
最初に表示される dsUser が"QUSER"で、次の crUser が「SQL スクリプトの実行」を接続する時のユーザー、つまり実行ユーザーになっていることが確認できますね。

さらに、System API を組み込んでみました。
最初の RtvJobInf プロシージャは、ジョブの実行情報を取得するものです。もともとユーザープロフィールを取ろうと思ったのですが、ジョブのユーザーで、実際の実行ユーザーではない場合があるので、プログラム状況データ構造に戻しています...... そういう意味では何にもこのプロシージャは使われていないわけなのですが、まぁ参考になるかなと思ってそのまま載せています。
取得した情報は DSPLY 命令ではなく、QMHSNDPM API を使って情報メッセージとして出力するように変更しました。
H DFTACTGRP(*no)
*
D SDS
D dsUser 254 263a
D crUser 358 367a
*
D Trg_SndMsg PR
D TrgBuffer LIKEDS(TrgInfo)
D TrgBufferLen 10i 0
*
D Trg_SndMsg PI
D TrgBuffer LIKEDS(TrgInfo)
D TrgBufferLen 10i 0
*
D TrgInfo DS Qualified
D File 10a
D Library 10a
D Member 10a
D Event 1a
D Time 1a
D CmtLckLvl 1a
D 3
D CCSID 10i 0
D RRN 10i 0
D 4
D BfrRecOfs 10i 0
D BfrRecLen 10i 0
D BfrNullOfs 10i 0
D BfrNullLen 10i 0
D AftRecOfs 10i 0
D AftRecLen 10i 0
D AftNullOfs 10i 0
D AftNullLen 10i 0
*
D BfrRecPtr S *
D Original E DS ExtName(QCUSTCDT)
D Based(BfrRecPtr)
D Qualified
*
D AftRecPtr S *
D New E DS ExtName(QCUSTCDT)
D Based(AftRecPtr)
D Qualified
*
D Insert C '1'
D Delete C '2'
D Update C '3'
D Read C '4'
*
D After C '1'
D Before C '2'
*
D none C '0'
D change C '1'
D cs C '2'
D all C '3'
*
D RtvJobInf PR EXTPGM('QUSRJOBI')
D RcvVar 65535a Options( *VarSize)
D RcvVarLen 10i 0 Const
D FmtName 8a Const
D QualJobName 26a Const
D InternalJobID 16a Const
D ErrorCode like(APIErr)
*
D QualJobName DS 26 Qualified
D JobName 10a Inz('*')
D UserName 10a Inz(*blanks)
D JobNumber 6a Inz(*blanks)
*
D JOBI0400 DS Qualified
D NbrBytesRtn 10i 0
D NbrBytesAvl 10i 0
D JobName 10a
D UsrName 10a
D JobNbr 6a
D InternalJobID 16a
D JobSts 10a
D JobType 1a
D JobSubType 1a
D InTimestamp 13a
D ActTimestamp 13a
D JobAcctCode 15a
D JobDName 10a
D JobDLName 10a
D UOWID 24a
D ModeName 8a
D InqMsgRpy 10a
D LogCLPgm 10a
D BrkMsg 10a
D StsMsg 10a
D DevRcyAcn 13a
D DDMCnv 10a
D DatSep 1a
D DatFmt 4a
D PrtTxt 30a
D SbmJobName 10a
D SbmUsrName 10a
D SbmJobNbr 6a
D SbmMsgqName 10a
D SbmMsgqLName 10a
D TimSep 1a
D CCSID 10i 0
D DatTimScdJob 8a
D PrtKeyFmt 10a
D SrtSeqTbl 10a
D SrtSeqLib 10a
D LangID 3a
D CntryID 2a
D CompSts 1a
D SignonJob 1a
D JobSwt 8a
D JobMsgqFl 10a
D 1a
D JobMsgqMx 10i 0
D DftCCSID 10i 0
D RtgDta 80a
D DecFmt 1a
D ChrIdCtl 10a
D SvrType 30a
D AlwMltThd 1a
D JobLodPnd 1a
D 1a
D JobEndReason 10i 0
D EJobType 10i 0
D DatTimeJobEnd 13a
D 1a
D SplFAcn 10a
D Ofs2ASPinf 10i 0
D NbrEntASPinf 10i 0
D EntryLen 10i 0
D TimeZoneD 10a
D JobLog 10a
*
D APIErr DS Qualified
D ErrSize 10i 0 inz(%size(APIErr))
D ErrLen 10i 0 inz(0)
D ErrID 7a
D rsvd 1a
D ErrData 32767a
*
D RcvSize S 10 0 INZ(16776704)
*
D HandleErr PR
*
D SndPgmMsg PR EXTPGM('QMHSNDPM')
D MsgID 7a Const
D QualMsgF 20a Const
D MsgDta 256a Const
D MsgDtaLen 10i 0 Const
D MsgType 10a Const
D CallStkEntry 10a Const
D CallStkCount 10i 0 Const
D MsgKey 4a Const
D ErrorCode like(APIErr)
*
D MSG S 3000a inz(*blanks)
D MsgKey S 4a
*
/Free
BfrRecPtr = %addr(TrgBuffer) + TrgBuffer.BfrRecOfs;
AftRecPtr = %addr(TrgBuffer) + TrgBuffer.AftRecOfs;
RtvJobInf( JOBI0400 :
RcvSize :
'JOBI0400' :
QualJobName :
*blanks :
APIErr );
If APIErr.ErrLEN <> 0 ;
HandleErr() ;
return ;
endif ;
// MSG = ('USRPRF ' + //
// %trim(JOBI0400.UsrName) + ' saw: ' + //
MSG = ('USRPRF ' +
%trim(crUser) + ' saw: ' +
%trim(TrgBuffer.Library) + '/' +
%trim(TrgBuffer.File) + '(' +
%trim(TrgBuffer.Member) + '):' +
'RRN=' +
%trim(%char(TrgBuffer.RRN))) + ' ' +
(%trim(%char(Original.CUSNUM)) + ' ' +
Original.LSTNAM +
' ' + Original.STATE);
SndPgmMsg( 'CPF9897' :
'QCPFMSG *LIBL' :
%trim(MSG) :
%len(%trim(MSG)) :
'*INFO' :
'*PGMBDY' :
1 :
MsgKey :
APIErr );
// *inLR = *on ; //
return ;
/End-Free
*
*
P HandleErr B
D HandleErr PI
*
/Free
/End-Free
*
P HandleErr E
|
RUNQRY コマンドでもちゃんとトリガーは起動されます。

こちらは RUNQRY の実行結果です。

トリガーからの出力はジョブログに載っています。

|
|