RPG OA: ハンドラ・プログラムを使用したデータ・アクセスのモダン化とパフォーマンスの向上 パート2
ハンドラ・プログラムとSQLのブロックFETCHルーチンを使用してRPGプログラムでのデータ・アクセスをモダン化する
「Open Access for RPG (RPG OA)によるプログラムのパフォーマンスの向上」でご紹介した概念をご理解いただいているかと思います。要約すると、IBM Rational Open Access: RPG Edition (別名Open Access for RPGまたはRPG OA)で提供されているRPG IVの拡張であるHANDLERキーワードについて紹介し、この機能を使用することにより複数の入出力操作を遮って、SQLのより高度な機能を使用するより効率的でモダンな方法に変換する方法について説明しました。そしてRPGプログラムCHAINGANGRでのデータ・アクセスをモダン化してそのデータ・アクセスのメカニズムを複数のファイルに対するランダム入出力操作の組み合わせではなく、複数のテーブルをジョインしたSQLビューに対してSQLブロックFETCHを使用することで、ランダム入出力操作の回数を劇的に減らしプログラムのパフォーマンスを向上させる手順を紹介しました。
パート1では4つのステップのうちの2つのステップについて説明しました。すなわち、テスト用のデータベースの作成とSQLビューの作成です。さて以下の残りの2つのステップを実行することで処理を完成させましょう。
- SQLを使用してSQLビューにアクセスするハンドラ・プログラムを作成
- 上記ハンドラを使用するように既存のILE RPGプログラムを修正
データ・アクセス層の主な目的はSQLビューを介して物理データベースへのアクセスを提供することです。SQLプロシージャやRPGハンドラ・プログラムの書き方については本稿の対象範囲ではありません。SQLハンドラの作成やそのサンプル・コードへのリンクについては記事「Rational Open Access: RPG Editionを使用したRPGデータベース入出力の分離.」を参照してください。
前述の記事にあるコードと本稿のシリーズのコードには大きな違いがいくつかあります。まず、複数のファイルに対して1つのハンドラを使用していることです。2つ目はrpgIO.userAreaポインタとrpgIO.stateInfoポインタを使用していることです。そして3つ目はMainコントロール・レベル・キーワードを使用していることです。処理全体に変わりはありませんが、コードを多少追加する必要があります。ハンドラを非メイン・プログラムとして定義するのに使用したコードを図1に示します。
図1では、RPGプログラムからハンドラに渡されたOpen Accessデータ構造の名前がrpgIO となっています。この名前を使用してコード例中で使われているサブ・フィールドを見分けます。フォーマット・ベースのハンドラと複数のファイルを定義するコードの概要を図2に示します。
rpgIO.userAreaポインタをベースとしてデータ構造を定義する
Open Access for RPGデータ構造のrpgIO.userAreaサブ・フィールドはRPGプログラムによって割り当てられた格納域へのポインタです(これについてはあとでもう少し説明します)。この構造を使用してRPGプログラムからハンドラへ追加の情報を渡すことができます。たとえば、処理プログラム側で使用するブロッキング・ファクタやSQL文字列を渡すことができます。
rpgIO.stateInfoはハンドラによって割り当てられたポインタです。このポインタは制御がRPGプログラムに戻った後でも永続的に残る格納域にセットされていなければなりません。rpgIO.userArea格納域はその良い候補です。呼び出し側のRPGプログラムがそれを定義しているからです。 rpgIO.stateInfoポインタとrpgIO.userAreaポインタをベースとしたデータ構造のコード例を図3に示します。
図3: rpgIO.stateInfoポインタとrpgIO.userAreaポインタをベースとしたデータ構造
この例では、rpgIO.userAreaポインタとrpgIO.stateInfoポインタを組み合わせて使うことで処理対象のファイル間で情報を共有しています。サブ・フィールドが別のハンドラ・ルーチンのコード内で使用されるのを説明する時に、このデータ構造名を使用してサブ・フィールドを見分けます。
RPGのファイル・オープン操作の処理
各ファイルは、RPGファイル・オープン時にハンドラを(暗示的にまたは明示的に)起動してRPG入出力フィードバック領域とrpgIO.stateInfoポインタを確立する必要があります。 rpgIO.stateInfoポインタのサイズはstateInfoデータ構造テンプレートのサイズに応じて割り当てられます。 rpgIO.stateInfoポインタを割り当てた後でrpgSI.sharedData_pポインタにrpgIO.userAreaポインタの値がセットされます。図4に示すコード部分はstateInfoポインタとフィードバック領域用の空間を割り当てています。フィードバック領域の詳細についてはRPG Open Accessのドキュメントを参照してください。
図4: stateInfoポインタとフィードバック領域用の空間の割り当て
SQL PREPAREは指定されたプライマリ・ファイルであるEMPPRJACTのファイル・オープン時に一度だけ実行しなければなりません。それはハンドラがSQLジョイン・ビューにアクセスするからです。rpgIO.externalFile.nameの値がEMPPRJACTと等しければ、SQL文字列を構築して準備します。SQL文字列にはPROJNO変数用のパラメータ・マーカが含まれています。その他のすべての処理対象ファイルについては図5に示した通りrpgStatusを0として返すだけです。
SETLLとSQL OPENの処理
PROJNOの値はRPGのオープン時にはわからないので、SQL OPENは最初のRPG入出力操作、今回の場合はSETLL、が行われるまで待たされます。EMPPRJACTファイルのキー構造を使用してデータ構造「keys」が定義されます。このデータ構造用の格納域はrpgIO.keyポインタをベースとしています。PROJNOキー・フィールド・データは検索引数として使用されSQL OPEN CURSORプロシージャに渡されます。RPG SETLLをSQL OPEN CURSORに変換する処理を図6に示します。
SQL OPEN CURSOR文が実行され、USING節を介してPROJNOの値をDB2 for iに渡します。PROJNOの値は準備したSQL文中のパラメータ・マーカ(?)用の置換変数です。
SQL OPENプロシージャで、SQL OPEN CURSOR文の実行が成功した直後にGET DIAGNOSTICS文が実行されます。GET DIAGNOSTICS文を使用してそれまでに実行されたSQL文の情報を返します。結果セット中の総行数がわかっていればDB2_NUMBER_ROWS文の情報項目を使用してそれを要求します。ゼロより大きな値が返されてきたときはそれが概算値であるという点に注意してください。
DB2_NUMBER_ROWSの値を格納するのに使用されている変数(v_Total_Rows)はパック変数(31,0)として定義されています。この値は共有のuserAreaデータ構造(rpgUA)フィールドを以下のようにセットするのに使用されます。
- rpgUA.hostArray_pポインタ用の格納域を割り当てる
- SQLブロックFETCHルーチン中で使用されているrpgUA.rowsRequestedの値をセットする
- v_Total_Rowsの値がホスト配列中の要素の総数よりも小さい場合はrpgUA.db2LastRowインジケータを*ONにセットする
SQL文の複雑度によっては行の総数がわからない場合もあるので、DB2_NUMBER_ROWSの値がゼロになる場合もあります。このような場合はホスト配列の出現回数をデフォルト値として使用します。number-of-rowsの値はSQL FETCHの後で再検証されます。
RPGに戻る前にRPG foundインジケータとRPG equalインジケータをオンにセットしなければなりません。こうすることでRPGプログラム内のRPG SETLL操作の直後の RPG IF %EQUALテストの条件が満たされます。
SQLブロックFETCHルーチンの実行
論理的にはRPG READE操作を処理しているときにSQL FETCHプロシージャを実行します。オプションとして、RPGに戻る前に最初のSQL FETCH操作をオープン・カーソル・ルーチンから実行することができます。図6に示したコード例では、getBlockedDataという共通のサブ・プロシージャを実行することでこれを実現しています。
getBlockedDataサブ・プロシージャはSQL FETCH NEXTルーチンを実行し、rpgUA.rowsRequestedとrpgUA.hostArray_pを渡します。getBlockedDataサブ・プロシージャは最初の読み出し操作の際に一度だけ呼び出され、すべての行がホスト配列から返されてrpgUA.db2LastRowが*OFFになったときにもう一度呼び出されます。
SQLブロックFETCHルーチンはすべての読み込み操作(READE、READ、CHAIN)に対して使用することができます。CHAINの場合、rpgUA.rowsRequestedの値は1になります。複数のファイルに対して1つのSQLジョイン・ハンドラを使用するときは、ホスト配列をジョインされるファイルと共有しなければなりません。sqlView_tテンプレートのレコード・フォーマットのようなグローバル・ホスト配列(この場合、複数出現するデータ構造)を定義し、sqlViewHostArray_pポインタをベースとすることで共有することができます。これを実行しているコードを図7に示します。
SQL FETCH NEXTルーチンはフェッチされた実際の行数と最後の行のインジケータを返します(図8)。
SQL GET DIAGNOSTICS関数はSQL FETCHプロシージャの中でSQL FETCHが正常に実行された直後に実行されます。SQL GET DIAGNOSTICSプロシージャはフェッチした行数を提供するだけでなく、戻された行に結果セットの最後の行が含まれているかを調べるのに使用することもできます。これにはGET DIAGNOSTICSから返されたDB2_LAST_ROWの値に等しい整数変数を設定することでできます。値が0ということは結果セットが完全でないことを、+100であれば最後の行が結果セットに含まれていることを意味しています。後者の場合は、SQL FETCHルーチンはend-of-fileインジケータをgetBlockedDataサブ・プロシージャに返します。
注記: rpgUA.rowsRequestedの値が(SQL OPENルーチンで実行されたGET DIAGNOSTICSをベースとした)結果セット中の行数に等しく、しかも実際にフェッチした行数に等しい場合は、結果セット全体をフェッチしたとしても、DB2_LAST_ROWには+100が含まれていないかもしれません。結果セットが完全であるとわかっている場合は、DB2_LAST_ROWインジケータに手操作でセットします。
RPGのREADE操作とCHAIN操作の処理
Handle_Readサブ・プロシージャは各READE操作のために実行されます。sqlViewHostArray_pポインタはSQL OPEN CURSORルーチンで割り当てられ、場合によってはSQL FETCH NEXTルーチンで調整されたrpgUA.hostArray_pの値をベースとしてセットされます。出現変数rpgUA.Current_rowはHandle_Setllサブ・プロシージャ中で0にセットされ、Handle_Readサブ・プロシージャが呼び出されるたびに1ずつ増えていきます。
rpgUA.Current_rowの値がrpgUA.rowsFetchedの値より大きくてrpgUA.db2LastRowインジケータが*ONになっている場合は、結果セット全体が処理されています。ハンドラはrpgIO.eofを*ONにセットすることでend-of-fileインジケータを返します。rpgUA.db2LastRowが*OFFの場合はgetBlockedDataサブ・プロシージャが呼び出されて次の行のブロックが取り出されます。このどちらの条件も成立しない場合、Handle_ReadはReturn_A_Row_From_An_Arrayサブ・プロシージャを呼び出してrpgUA.Current_row、rpgIO.externalFile.name、rpgIO.inputBufferポインタをパラメータとして渡します。
Handle_Chainサブ・プロシージャはReturn_A_Row_From_An_Arrayサブ・プロシージャを呼び出してHandle_Readサブ・プロシージャで確立された値を渡します。チェーンされるファイルのデータがREADE操作によって処理されたファイルのデータと同じジョインされた行に含まれているため、rpgUA.Current_rowの値は増えません。
Return_A_Row_From_An_Arrayサブ・プロシージャ中で、LIKERECキーワードを使用してEVAL-CORR RPG操作のターゲット構造を定義します。この構造はrpgIO.inputBufferポインタをベースとしています。実際のファイルの定義を図2に示します。rpgUA.Current_rowの値を使用してSQL_View_Host_Arrayデータ構造の現在の出現をセットします。rpgIO.externalFile.nameを使用して適切なEVAL-CORR操作を選択します。基本的に、フェッチしたデータはRPGとSQLビューのレコード・フォーマット中の同じ名前を持つ列に対してのみ移されます。この処理を図9に示します。
今説明したプロシージャはリクエストされた行がすべて処理されるまで繰り返されます。対話型のサブ・ファイルのロード・ルーチンでは128行かもしれませんし、夜間バッチ・ジョブ処理では1億2,800万行かもしれません。
RPGのクローズ操作の処理
Handle_Closeサブ・プロシージャは各処理対象ファイルがクローズされるたびに1度だけ呼び出されます。クローズしたファイルがユーザーのコントロール下にある場合は明示的に呼び出され、RPGプログラムがLast Record (*INLR)インジケータを*ONにしたためにプログラムがアクティブでなくなった場合は裏側で行われます。Handle_Closeサブ・プロシージャはrpgIO.externalFile.nameの値がEMPPRJACTであるかを調べます。これによりSQL Close Cursorルーチンが呼び出され、適切なSQLカーソルをクローズします。クローズされる各ファイルに対してrpgIO.stateInfoポインタの割り当てが解除されます。この処理を図10に示します。
まとめ
2020年までにはソーシャル・ネットワーキングの一極集中化とモバイル技術の進歩、そして収集される情報量の増大が相まって、一度に1レコードずつデータを処理する方法で開発されている今日のシステムは破たんするでしょう。別の言い方をすれば、データの山の中に埋もれてしまうということです。これは今日でも、42億行のファイル制限に到達するようなテーブルの増加や、要求時間内に完了することのできないシングル・スレッドのアプリケーションに見られます。
本稿のシリーズのパート1では、一度に1レコードずつデータを処理する方法をSQLセット・ベースのデータ・アクセス方法に置き換えることでメモリ(論理)入出力操作の回数を削減できるということを図に示しました(図11)。
従来は、SQLを使用するように既存のプログラムを変換するためにはプログラムの中の複数行のコードを変更しなければなりませんでした。Open Access for RPGが追加されたことで、プログラムの大規模な変更、起こりうるビジネス・ロジックの変更はもはや必要ありません。1行追加するだけで、入出力を伴うシングル・スレッドのアプリケーションが最新のPower処理およびSQL技術を利用することができるように変換することができます。一度に1レコードずつデータを処理する方法がSQLセット・ベースのアクセス方法を使用することで大きな恩恵を受けられる例を図12に示します。
図12: 一度に1レコードずつデータを処理する方法とSQLによる代替方法
確かにハンドラ・プログラムの取得やコーディングが必要になります。確かにハンドラ・プログラムは日常のRPGプログラムでは通常使用されない高度なコーディング技術を使用します。しかし、すべての人がハンドラのプログラマになる必要はないですし、すべてのプログラムやファイルを処理する必要もありません。IBMのデータ・アクセスのモダン化戦略においては、データベースのリエンジニアリング・チームを構築することを強くお勧めします。このチームは、最新の開発者向けの強力なツールを使用して、前世紀の技術を使用して大量のデータ処理に苦労しているプログラムを改良するのに必要なハンドラ・ルーチンを開発することができます。