1回のSQLフェッチで可変次元配列をロードする
様々なショップで仕事をすることについて私が本当に気に入っている1つのことは、膨大な量のソース コードに触れられることです。しかし、それが一番ではありません。一番なのは、私が出会い、知り合う人々です。クライアントが多ければ、出会える師匠も多くなります。ソース コードと人々が結び付くことで、私の人生は、個人的にも職業的にも豊かなものになります。
ここで、製造業を営むITショップの業務で私が学んだSQL手法を紹介することができて私は嬉しく思います。プログラマーはこの手法を使用して、RPGプログラムでSQLカーソルからサブファイルをロードします。これは、クライアントサーバー アプリケーションでも同様に機能します。また、これは複数行フェッチと 可変次元配列を組み合わせます。結果として、データを取得および操作する簡単で強力な方法ということになります。それでは、どういうことなのか見て行きましょう。
複数行フェッチは、1回の操作でデータベースから複数の行をメモリーに取得します。このタイプのフェッチでは、次のように、 for n rows 節が fetch ステートメントに追加されることが必要となります。
Exec Sql
Fetch C1 for 250 ROWS INTO :ResultSet;
ResultSetは、データ構造の配列です。
このショップでは、プログラマーが、1回のフェッチで可変次元配列をロードする方法を見つけていました。次のような方法です。
- カーソルを宣言してオープンする。
- Get Diagnosticsを使用して、結果セット内の行数を取得する。
- %elem組み込み関数を使用して、結果セット配列の次元を設定する。
これだけです。ただし、あと一点だけ。プロセス全体を狂わせることもある、ちょっとした問題が1つあります。すなわち、カーソルは insensitiveでなければなりません。
時折、私たちは、センシティビティ オプションのことを忘れてしまいます。すなわち、sensitive、insensitive、およびasensitiveのことです。このオプションは、データがどれくらい最新である必要があるかを指定します。基になる表に対してユーザーが挿入、更新、および削除するたびに、変更内容を確認する必要がある場合は、sensitiveカーソルが必要です。
私がプログラムに埋め込んできたSQL照会のほとんどでは、センシティビティは重要ではありませんでした。しかし、このケースでは非常に重要です。カーソルがinsensitiveでない場合、Get Diagnosticsは正確な行数を返しません。
データが配列にコピーされたら、たとえばルックアップおよびソートなどの配列オプションの一式をすぐに利用できるようになります。
これがどのように機能するか説明するために、小さなサンプル プログラムを用意しました。
**free
ctl-opt actgrp(*new) main(PCTL0013R)
option(*srcstmt: *nounref: *nodebugio)
debug(*RetVal);
exec sql set option datfmt=*iso;
dcl-ds ResultSet ExtName('PRODUC01') qualified alias dim(*var: 100);
end-ds;
dcl-s ResultSet_Rows uns(10) ;
// Sort sequences
dcl-enum SortBy qualified;
Order 'ORDER';
Customer 'CUSTOMER';
DueDate 'DUEDATE';
end-enum SortBy;
dcl-proc PCTL0013R;
dcl-pi *n;
inPartNumber char(3) const;
end-pi;
MainRoutine (inPartNumber);
end-proc PCTL0013R;
dcl-proc MainRoutine;
dcl-pi *n;
inPartNumber char(3) const;
end-pi;
LoadResultSet (inPartNumber);
SortResultSet (SortBy.Order);
SortResultSet (SortBy.Customer);
end-proc MainRoutine;
dcl-proc LoadResultSet;
dcl-pi *n;
inPartNumber char(3) const;
end-pi;
Exec Sql Declare PCTL0013R_1 insensitive Cursor for
select p.*
from ProductionView as p
where p.PartNumber = :inPartNumber
order by p.DueDate, p.OrderNumber
for read only;
Exec Sql Open PCTL0013R_1 ;
Exec Sql Get Diagnostics :ResultSet_Rows = DB2_NUMBER_ROWS ;
%Elem(ResultSet) = ResultSet_Rows ;
If ResultSet_Rows > *zero ;
Exec Sql
Fetch PCTL0013R_1 for :ResultSet_Rows ROWS INTO :ResultSet;
EndIf ;
Exec Sql Close PCTL0013R_1 ;
end-proc LoadResultSet;
dcl-proc SortResultSet;
dcl-pi *n;
inSequence char(12) const;
end-pi;
select inSequence;
when-is SortBy.Order;
SortA ResultSet %Fields(CustomerNumber: DueDate);
when-is SortBy.Customer;
SortA ResultSet %Fields(OrderNumber);
other;
SortA ResultSet %Fields(DueDate);
EndSl;
end-proc SortResultSet;
ある工場で製造スケジュールを担当しているエンド ユーザーがいるとします。このプログラム(PCTL0013R)を使用して、このスケジュール担当者が作成済みであってほしいと思っている部分のデータを取得します。データは、データベース エンジニアがビュー(ProductionView)を作成した4つの表(セールス オーダー ヘッダー、セールス オーダー詳細、顧客マスター、および品目マスター)にあります。ビューのシステム名はPRODUC01です。
以下は、SQL結果セット用のデータ構造の可変次元配列です。
dcl-ds ResultSet ExtName('PRODUC01') qualified alias dim(*var: 100);
end-ds;
aliasキーワードは、配列がProductionView内の列名と同じフィールドを持つようにします。正常に機能するためにはシステム名を必要とする古いプログラムで作業している場合を除いて、通常は、より長いフィールド名を利用できるように、 alias キーワードを使用します。
ResultSetをモジュールレベル(グローバル)変数として定義していることに注目してください。私は、グローバル変数が嫌いなことについて何度も述べてきましたが、 変数をグローバルに宣言するのが合理的なケースもあります。特に、その変数がプログラム全体の軸となっている場合です。
以下は、カーソル定義です。
Exec Sql Declare PCTL0013R_1 insensitive Cursor for
select p.*
from ProductionView as p
where p.PartNumber = :inPartNumber
order by p.DueDate, p.OrderNumber
for read only;
Exec Sql Open PCTL0013R_1 ;
カーソルは、insensitiveとして定義されています。お望みなら、 fscrollを追加することもできますが、私はスクロール可能なカーソルが必要だと感じたことはありません。一度データにアクセスするだけだからです。selectにfor read only 節を追加しました。それが必要であるかどうか私には分かりませんが、そのショップのプログラマーは、それが役に立つと述べています。それがSQLエンジンにより良い判断を行うためのより多くの情報を与えるかもしれないと私は思います。用心するに越したことはありません。
insensitiveカーソルがオープンしているので、Get Diagnosticsは、結果セットにいくつの行があるかを知っています。配列の次元を設定します。
%Elem(ResultSet) = ResultSet_Rows ;
これで、必要に応じて組み込み%ELEM関数を使用することができるようになり、それほど頻繁に%SUBARRを必要とすることはなくなります。
ちなみに、デバッガーで現在の配列の次元を確認する必要があるときは、いつでも_QRNU_VARDIM_ELEMS_RESULTSET変数を参照できます。RESULTSETをご自分の配列の名前に置き換えます。
行をフェッチします。
If ResultSet_Rows > *zero ;
Exec Sql
Fetch PCTL0013R_1 for :ResultSet_Rows ROWS INTO :ResultSet;
EndIf ;
これでカーソルの使用が完了しました。カーソルをクローズします。データは配列にあるので、これで自由に処理することができます。当該ショップはグリーンスクリーン インターフェースを使用しているため、彼らのプログラムは配列をサブファイルへロードします(配列をソートした後の場合もあります)。このサンプル プログラムには、ユーザー インターフェースがありません。そのようなプログラムは、データベース入出力を持たない呼び出しプログラムに、簡単にデータを渡すことができます。
先程、ルックアップおよびソートについて触れました。どの配列命令も使用することができます。サンプル プログラムにソート ルーチンを追加しました。
// Sort sequences
dcl-enum SortBy qualified;
Order 'ORDER';
Customer 'CUSTOMER';
DueDate 'DUEDATE';
end-enum SortBy;
SortResultSet (SortBy.Order);
SortResultSet (SortBy.Customer);
dcl-proc SortResultSet;
dcl-pi *n;
inSequence char(12) const;
end-pi;
select inSequence;
when-is SortBy.Customer;
SortA ResultSet %Fields(CustomerNumber: DueDate);
when-is SortBy.Order;
SortA ResultSet %Fields(OrderNumber);
other;
SortA ResultSet %Fields(DueDate);
EndSl;
end-proc SortResultSet;
列挙を使用して、ソート順序定数を定義してグループ化しました。引用符付きリテラルではなく、名前付き定数を使用するということは、コンパイラーがどのようなスペルミスも捕捉するということです。私は引き続き、列挙を使用するための効果的な方法を探します。
読者の皆さんにもこの手法を試していただけるように、ダウンロード可能なコード ファイルには、このプログラムと、サンプル データ作成のためのSQLを含めています。また、このRPGプログラムにはDumpResultSetサブプロシージャーが含まれています。私はこれを開発、テスト、およびデバッグ向けに含めることが多いですが、通常、クライアント プログラムはそれを呼び出しません。また、エラー処理コードを省略していることにも注意してください。本番プログラムでは、必要に応じてSQLSTATE変数をチェックします。
このようなデータ取得の手法を紹介してくれたことに対して、Mike Odom氏に感謝いたします。
いつものことですが、皆さんの使用体験を私に共有してください。特に、私が公開した記事の内容や、私が犯した間違いに対する改善案は大歓迎です。 『IT Jungle』の「Contact」ページからご連絡いただけます。
