Webサービス、DATA-INTO、DATA-GEN パート2
本シリーズのパート1では、DATA-GENとDATA-INTOを使って、Webサービスを利用して「ブログ記事」を作成する方法についてお話しました。今回は、GET HTTPメソッドを使って、ブログ記事を取得する方法を見ていきたいと思います。ご覧になれば分かると思いますが、基本的な処理はよく似ています。
まず、ブログ記事を1本だけ取得するところから始めようと思いますが、その記事に関するあらゆるデータを取得するのではなく、特定の要素のみの処理に制限する方法をお教えしようと思います。その後、複数の記事を処理する2種類のアプローチを見ていきます。1つ目はすべてのデータを一度に処理する方法、2つ目は「バッチ」処理する方法です。
Webサービスは前回使用したのと同じ基本URLを持っていますが、前回使用したPOSTではなくGET HTTPメソッドを使用します。GETリクエストの場合、WebサービスはURLの最後(つまり最後の「/」の後ろ)に記事番号があるかどうかをチェックします。記事番号がある場合、その特定の記事番号が取得されます。結果がどのようになるかを確認したい場合は、こちらをクリックしてください。ブログ記事番号15の「生」JSONデータが表示されるはずです。記事番号がない場合、すべてのブログ記事が返されます。このパラグラフの最初のリンクをクリックすると、それが確認できます。今回は両方の場合の例を使用します。
もちろん、ブラウザでこのように返されるJSONは、コードを書かずにWebサービスが動作するかどうかを確認するには便利ですが、大して実用的ではありません。そこで、前回と同様、DATA-INTOを使って解析してみます。先ほど見たように、ブログ記事番号15の場合、Webサービスは次のようなJSONを返してきます。
{
"userId": 2,
"id": 15,
"title": "eveniet quod temporibus",
"body": "reprehen derit quos ... fugiat vitae"
}
しかし、今回の演習の目的には、"body"要素に含まれるデータは必要ないと考えます。この要素は明らかに大きなサイズになるため、これを処理から除外することで、プログラムが使用するメモリ量を減らすことができます。また、処理速度も少し速くなるので、今回はこのDSを使用します。
Dcl-DS responseData Qualified Inz;
id int(5);
title varchar(30);
userid int(5);
End-Ds;
前述の通り、このサービスではGETメソッドを使用しており、パラメータはすべてURLの一部となります。その結果、HTTPAPIの呼び出しは、よりシンプルになります。その処理は次のようになります。
// Build URL to be used by adding blog Id to base URL
url = baseUrl + %Char(id);
...
response = HTTP_string( 'GET' : url );
Webサービスからの応答を得たら、DATA-INTOを使ってDSに流し込みます。コードは次の通りです。
Data-Into responseData
%DATA( response : 'case=any' )
%PARSER( 'YAJL/YAJLINTO' );
しかし、1つ問題があります。これをコンパイルして実行すると、「The document for the DATA-INTO operation does not match the RPG variable; Reason code 5(このDATA-INTO操作用のドキュメントは、RPG変数とマッチしません。原因コード5)」というようなランタイムエラーを受け取ることになります。
そして原因コード5を見ると、次のように定義されています。
「5. The document contains extra names that do not match subfields(5. ドキュメントにサブフィールドとマッチしない余分な名前が含まれている)」
この「余分な名前」というのは、もちろん、私が処理しないと決めたJSON内の"body"要素のことです。DATA-INTOがこのドキュメントを処理できるようにするには、%DATAオプション'allowextra=yes'を追加する必要があります。これはRPGに、ターゲットのDSに含まれていない要素がJSONソースの中に含まれていても構わないということを教えるためのオプションです。細かい調整はできないため、このオプションを使用するときは少し注意する必要があります。「"body"要素は無視してよいが、他の要素は存在していなければならない」ということはできないのです。そのため、Webサービスからの応答が変わってしまって別の要素が追加された場合、それに気づく術がないということを意味します。"allowextra"オプションが単純にそれらを無視する原因にもなってしまうからです。
追加後のコードは次のようになります。
Data-Into responseData
%DATA( response : 'case=any allowextra=yes' )
%PARSER( 'YAJL/YAJLINTO' );
これがすべてです。RPGプログラムUSEWEBSRV2(ここからダウンロードできます)全体のコードを見れば、プログラムはMONITORグループを使用してHTTPAPIから返されたあらゆるエラーをキャッチしていることが分かるでしょう。この特定のWebサービスでは、存在しないブログ記事の情報を取得しようとするとHTTPAPIがエラーを返します。ここでは分かりやすくするために、すべてのエラーをまとめて「Not Found」のステータスとして扱うことにしています。Webサービスの多くで、エラーの処理はこのような単純化したアプローチで充分です。そうではない場合には、返されたエラーメッセージを正確に処理してそれに従って適切な対処をする必要があるかもしれません。これについては、また次回以降にお伝えすることにしましょう。
複数の結果を処理する
先ほど、ブログ記事番号がURLの最後についていない場合には、Webサービスはすべてのブログ記事のリストを返すということをお知らせしました。では、このリストを処理するにはどうしたらいいのでしょうか?
このようなリストを扱うには2つのアプローチがあります。すべて一括で行うか、「塊」ごとに行うかです。1つ目のアプローチは、返されるアイテム数をコントロールできるか、ある数より絶対に大きくならないということを知っている場合に最適です。2番目のアプローチは、返される結果の数が分からない場合に最適です。また、データを部分ごとに処理できる場合にも有用です。例えば、結果がサブファイルで表示される場合です。サイズ制限が大きく緩和されたIBM i V6が登場する以前は、多くの場合、非常に大きなフィールド(説明など)を含む結果の処理を促進するためにこの2番目のアプローチを使う必要がありました。しかし、サイズ上限が大きくなったことから、このような場面は減りました。
まず、「すべて一括」アプローチから見ていきましょう。今回扱っているような単純な例では、必要となるのはDATA-INTOターゲットをDS配列にすることだけです。ターゲットの定義はこのように変わります。
Dcl-DS responseData Qualified Inz Dim(200);
id int(5);
title varchar(30);
userid int(5);
End-Ds;
配列のいくつの要素がDATA-INTOにロードされたかはどうしたら分かるでしょうか。RPGコンパイラは、プログラム状況データ構造(PSDS)の位置372から始まる8バイトのカウント(符号なしの20桁の整数)を提供します。私のプログラムでは、これを次の通り定義しました。
Dcl-DS *n PSDS;
itemCount Uns(20) Pos(372); // Populated by DATA-INTO
End-Ds;
必要なのはこれだけです。Webサービスの呼び出しに成功したら、itemCountに格納された値を使って配列要素の処理を制御することができます。私が作成したテストプログラムUSEWEBSRV3では、これを%LOOKUP操作によってスキャンされる要素の数をコントロールするのに使用しています。次にその様子を示します。
DoU forever; // the "forever" indicator is off and will never be set!
// Ask for Blog Post Id and exit when requested
Dsply 'Which post do you want to see? ( 0 to exit ):' ' ' item;
// Quit if user requests it
If item = 0;
Leave;
EndIf;
index = %LookUp( item : responseData(*).id : 1 : itemCount );
If index <> 0;
Dsply ( 'Title: ' + responseData(index).title );
Dsply ( 'Author: ' + %Char(responseData(index).userid) );
Else;
Dsply ( 'Post Id # ' + %Char(item) + ' not found' );
EndIf;
EndDo;
「塊」ごとに結果を処理する
この処理は単純です。DATA-INTO操作のターゲットをDSではなくハンドラのサブプロシージャに変更するだけです。このサブプロシージャが「塊」、つまりデータを受け取ります。元々は次のようになっています。
Data-Into responseData
%Data( response : 'case=any allowextra=yes' )
%PARSER( 'YAJL/YAJLINTO' );
それをこのように変更します。
Data-into %Handler( ProcessPosts : itemCount)
%Data( response : 'case=any allowextra=yes' )
%PARSER( 'YAJL/YAJLINTO' );
%HANDLERがDS名である responseDataの代わりに置かれたことに注目してください。DATA-INTOに限って言えば、他の変更は不要です。%HANDLERの1つ目のパラメータは、処理を行うサブプロシージャを識別します。2つ目は通信域と呼ばれ、DATA-INTOを含むコードと処理を行うサブプロシージャの間での情報のやりとりに使用されます。これが必要な理由は、RPGコードがサブプロシージャを直接呼び出しているわけではなく、RPGランタイムを通して間接的に呼び出しているからです。このような値の使用は、ハンドラのサブプロシージャのコードを見るとより明確に分かります。
先ほど、データを「塊」で処理するといいました。では、その塊のサイズはどのようにコントロールするのでしょうか。答えは非常に簡単ですが、すぐに理解できるものではないかもしれません。DIMを使用して一度に扱う要素の数をサブプロシージャのインターフェイスに指定するのです。私のテストプログラムUSEWEBSRV4のプロシージャインターフェイスはこちらです。
Dcl-Proc ProcessPosts;
Dcl-Pi *N Int(10);
count Int(5);
postData LikeDs(responseData_T) Dim(40) Const;
items Uns(10) Value;
End-Pi;
RPGがこのサブプロシージャに渡すパラメータは3つあります。
- 1つ目は、先ほど述べた通信域パラメータです。
- 2つ目は、事実上の-INTOターゲット変数です。これにはRPGが解析したデータが含まれています。ご覧の通り、このケースでは一度に40個のアイテムを処理することにしました。そのため、DIM(40)としています。一度に処理したいアイテムが1つだけでも、パラメータをDSとしてコーディングする必要があることに注意してください。つまり、DIM(1)とコード化する必要があるのです。
- 3つ目は、RPG が提供する、-INTO配列に入力された要素数のカウントです。私のDS配列には40個の要素があるので、この数は常に40になります。例外は最後の呼び出しで、1から40までのどれかの値になります。処理するものがない場合はハンドラが呼び出されないため、0になることはありません。
もう1つの注意点は、プロシージャが4バイトの整数(Int(10))を返すように定義されていることです。これは、サブプロシージャがRPGランタイムに処理を放棄するように通知するメソッドを提供します。これまで、RPGに処理の継続を指示する0の値を返す以外に何かすべきことはありませんでした。
サブプロシージャ内では、データを受信したことを示すために最小限の処理のみを行っています。以下はそのコードです。
Dsply ( 'Processing ' + %Char(items) + ' items' );
Dsply ( 'Starting with item ' + %Char(postData(1).id)
+ ' from user ' + %Char(postData(1).userid) );
count += items; // Increment total count
Return 0;
まず、第2パラメータとして渡されたDSにRPGで読み込まれた要素数のカウントを含む変数itemsを表示します。次に、DS配列の最初のアイテムからユーザーIDとブログ記事IDを表示します。次に、この呼び出しで処理されているアイテムの数をcount変数(つまり、通信域)に追加します。この例では、処理された要素の総数をメインライン・コードで利用できるようにするために使用しています。前の例では、すでにRPGがPSDSでこのカウントを提供していました。今度の例では「塊」で処理しているため、RPGはその値を提供できません。そのため必要に応じて構築することになります。ソースを確認すると、メインラインのDATA-INTO操作の次に値が表示されていることが分かります。
終わりに
ご覧のように、HTTPAPIとRPGのDATA-INTOを組み合わせることで、Webサービスとのやり取りが非常に簡単になります。もちろん、今回扱った例はとても単純なものですが、基本的なことはお分かりいただけたかと思います。より複雑な要件、特に要素の入れ子を含む要件を扱う際、多くの場合、受信側のデータ構造を適切に定義することが最大の課題となります。経験を積めば自然とできるようになりますが、最初のうちはScott Klement氏のユーティリティーYAJLGENが役に立つでしょう。このツールはYAJLライブラリに同梱されています。想定される応答JSONのサンプルを入力として取得し、必要なDSの最善の推測を生成してくれます。実際には、完全なDATA-INTOテストプログラムが生成されます。もちろん、フィールドや配列のサイズについては推測の域を出ませんが、生成されたDSの冒頭に含まれるコメントを見れば一目瞭然です。
次回
本シリーズの次回の記事では、DATA-INTOとDATA-GENを、RPG名にマッピングされない名前を持つ要素を含むJSONの処理にどのように活用できるかをお話しします。YAJLINTOパーサーとYAJLDTAGENジェネレータの他の機能についても少し触れようと思います。
質問や、この幅広い領域の中で今後もっと深掘りしてほしいところがあれば、ぜひお知らせください。