IBM iの専門家ジョン・パリスとスーザン・ガントナーが、データ構造と監査要求への対応について有用なヒントを共有します。
12/16/2019 ジョン・パリス&スーザン・ガントナー
新しいRPGの機能を説明する代わりに、どのような形にせよ皆さんの多くが出くわすであろう問題へのアプローチについて書くことに重点を置くべきだと考えました。よくあることですが、この記事はIBM iインターネット・フォーラム(今の場合code400.com)での質問に「触発」されました。
上述の質問者が言うには、2つのデータ構造(DS)があり、両者とも同じ外部記述に基づいています。1つ目のDSはテーブルから読み込まれた元々のデータを、2つ目は画面から入力された更新された(可能性のある)内容を保持しています。変更が行われたかどうかを決定するのは単純です。単に一方のDSを他方のDSと比較するだけです。もし、両者が一致しなければ変更が行われていると分かります。
さて、微妙な部分が生じました。質問者は、修正された特定のフィールドを識別するために監査レコードを書く仕事をしてきました。明らかに、1つのアプローチは順々に各フィールドを比較し、適切に報告するコードを書くことでしょう。しかし、気の遠くなるような退屈なコーディングはさておき、新しいフィールドが追加される度にそのコードは更新する必要がありますし、「醜い」の一言でしょう。おまけに、コードはこの監査が求められるすべての表ごとにその表専用に書かれなければなりません。「ただこの一表だけだ」と言われながら決して一表では終わらないので、もっと包括的なアプローチが必要だと私たち全員が知っています。もっと良い方法が必要です。
確かにそのような方法はあり、問題のフィールドは比較的簡単に識別できます。基本的な要件はDS内の各フィールドの開始位置と長さを決定する手段をもつことです。それにより、サブストリング操作を使うことでDSの関連する部分を比較できます。いくつかのフィールドが数値フィールドであるという事実はこの段階では何ら違いを生じません。私たちは、最初は値を格納するのに使用されているバイトを比較することだけに関心があります。
このような外部記述されたDSについての必要な情報を取得するために考えられる方法は、少なくとも3つあります。1つの方法は、SQLを使って、必要なデータすべてを含むSYSCOLUMNSまたはSYSCOLUMN2ビューにアクセスすることです。もしくは、QUSLFLD APIを使うこと、あるいは古き良きDSPFFDコマンドでOUTFILEを指定する方法(最後に書きましたが重要度が低いということではありません)があります。読者の多くはDSPFFDに馴染みがある可能性が高いので、例としてこの方法を選ぶことにします。
DSPFFDのOUTFILEにはフィールド毎に1行と、私たちが必要としない大量の情報が含まれています。私たちは、この例に必要な情報を格納するためのDS配列を考案しました。それは以下の通りです。
dcl-ds fieldDetail Dim(99) Qualified; startPosn like(WHIBO); // レコード内のオフセット length like(WHFLDB); // バイト長 name like(WHFLDI); datatype like(WHFLDT); decimals like(WHFLDP); // 小数桁の数 End-Ds; |
次に、DSPFFDのOUTFILEを読み込み、かつ必要な値を配列内のそれぞれの位置にコピーしてこの配列に値を格納しました。
これが必要な準備作業のすべてであり、プログラムの開始時に動的に実行できます。
変更されたフィールドの識別
後続のロジックを単純化するために最初に行うのは、処理しているフィールドのバイト列のコピーを作成することです。これは、フィールドの開始位置(fieldDetail(i).startPosn)からフィールドの長さ(fieldDetail(i).length)分のDSのサブフィールドを取ってくることで行われます。変更前データと変更後データのサブストリングは、可変長フィールドfieldBeforeとfieldAfterに格納されます。ここで可変長フィールドを使うことで、その後のロジックが簡単になります。長さ256バイトで、この例で必要な大きさよりも遥かに大きいものです。用途によって、これは変わる可能性があります。
フィールドの事前および事後の状態の一時コピーを獲得すれば、それらが変更されたかどうかを調べるために、それらを比較することができます。変更されていて、それが知る必要のあることのすべてであれば、仕事は完了です。しかし、質問のケースでは、変更されたフィールドの新しい値を知りたかったのです。ですから、次にどうやってこれを行うかを見ることにします。
dcl-s fieldBefore varchar(256); // どのフィールド長も256バイト以下 dcl-s fieldAfter varchar(256); // であると想定しています。 ... For i = 1 to count; // 後続のロジックを簡単にするために、変更前/変更後のイメージの // 関連する部分を一時フィールドにコピーする。 fieldAfter =%subst(after:fieldDetail(i).startPosn:fieldDetail(i).length); fieldBefore = %subst(before:fieldDetail(i).startPosn:fieldDetail(i).length); if fieldAfter <> fieldBefore; // フィールドが変更されていることを報告する。 // 変更前/変更後の値を報告するために、ここにロジックを追加する。 Dsply ('Field ' + fieldDetail(i).name + ' has changed'); |
数値の処理
このタスクの技法を明示するために、数値フィールドの最も一般的な型、ゾーンとパックの処理の仕方を示します。このタスクを遂行するために、この両タイプのフィールドの処理が行える以下の単純なDSを使いました。
dcl-ds mapNumeric; zonedValue zoned(15); packedValue packed(29) Overlay(zonedValue); end-Ds; |
要するに、私たちが行う必要があるのは、数値を含んでいるバイト列をmapNumericフィールドに右詰めにコピーすることです。その後、私たちは必要に応じてzonedValueまたはpackedValueフィールド経由で同じ数値にアクセスすることができます。
しかし、このやり方には1つ問題があります。123という値を表す3桁のゾーン十進フィールドがあると仮定します。これを表す3バイトには16進数x’F1F2F3’が含まれていますが、15桁のzonedValueフィールド内で正しい値にするには、前方を一連のx’F0’で埋める必要があります。同様に、パック十進の123は、16進数x’123F’という2バイトで表されますが、残念なことにパック十進数の先行文字はすべてx’00’でなければなりません。
この問題を解決するために、ゾーン十進数を扱う場合にはzonedValueにゼロをセットし、パック十進数を扱う場合にはpackedValueに同じことをします。その後、mapNumericの右端の位置を、数値を含むバイトで上書きする処理に進みます。元の値がたった3桁であっても、数値の先行位置が確実に正しい値で初期化されるようにしています。これが完了すると、残る作業は元の数値と同じ小数桁に合う様に小数点の位置合わせをすることだけです。下記のパック十進数値を処理するコードを見て、このことを学びましょう。ゾーン十進数も同じパターンになり、ダウンロード可能なコード例で学習することができます。
// mapNumeric DS内で数値フィールドデータがどこで始まるかを調べる。 (A) start = %Len(mapNumeric) - fieldDetail(i).length + 1; If fieldDetail(i).dataType = PACKED; packedValue = 0; // パック作業フィールドを初期化する。 (B) %subst(mapNumeric: start ) = fieldAfter; // 数値の小数桁を合わせる。 If fieldDetail(i).decimals > 0; (C) decimalValue = packedValue / (10 ** fieldDetail(i).decimals); Else; (D) decimalValue = packedValue; EndIf; (E) Dsply ('New value ' + %Char(decimalValue) ) ; |
(A)でmapNumericというDS内の開始位置を計算し、(B)の右端の位置バイトをコピーできるようにします。上記の例では、mapNumericは15バイトの長さで、3バイト(つまり5桁のパック十進数)をコピーすると仮定すると、開始位置は15 – 3 + 1 = 13 バイト目となります。ですから、データは13、14、15バイトにコピーされます。これは私たちの望む結果です。
(C)でフィールドの小数位置の情報を使い、packedValueの小数点位置合わせを行っています。小数点が無い場合には、(D)でpackedValueが結果フィールドにそのままコピーされます。
最後に(E)で処理結果を表示しています。もちろん、実際のプログラムでは更新された「事後」の値と同様に「事前」の値も取得することになるでしょう。その後、報告書またはディスク上の表に適切な監査レコードを書くことになるでしょう。
おわりに
この監査機能は多くのプログラムで使われる可能性があるので、通常これをサービスプログラムにします。しかし、あなたが簡単にこれをダウンロードし、実験できるようにするために、これをすべて1つのプログラムの中に入れました。
このテクニックは、何ができるかの例として示されています。明らかに、これは包括的な解決策ではありません。たとえば、プログラムすべてを見ると、decimalValueというフィールドが小数部5桁をもつフィールドとして定義されているのが分かります。その結果、小数部5桁未満の変更されたフィールドは、すべて末尾におまけの0を伴って表示されることになります。もし、あなたが監査報告書を作っているだけならば、これは問題にはなりません。単にdecimalValueフィールドに小数点以下の桁数をセットして、任意のフィールドが使い、かつあなたが用意できている最大の数を保持できるようにします。
また、あなたがどのように文字フィールドを処理するか決めなければなりません。可変長フィールドという考え得る例外はあるものの、これらは追加処理を必要としません。しかし、あなたはそれらを報告する方法と、プログラムが処理できるフィールドの最大長をいくつにするべきかを決めなければなりません。
さらに、表に任意のグラフィック列(例えば2バイト文字)またはUCS-2データ、BLOB、CLOB等々が含まれている場合、あなたはこれらを処理するために追加のテクニックを開発する必要があるかもしれません。
著者紹介
ジョン・パリスはTechChannelのテクニカル・ライターです。
スーザン・ガントナーはアプリケーション開発分野で30年以上のキャリアをもっています。