Webサービスは、今や一般的なアプリケーションの1つになっていますが、IBM iではJavaなどのRPG以外の言語を使用して対応するのが一般的だったのではないでしょうか。しかし、RPGにも新たにDATA-INTOやDATA-GENという命令が追加され、RPGでWebサービスを構築できるようになっています。そこでまず、第12回、第13回ではデータ交換形式のJSONについての基本的な知識と扱い方を、第14回、第15回ではDATA-INTOとDATA-GENによるWebサービスのコーディングに関する基礎知識をご提供するために翻訳解説記事をお届けすることにします。
DATA-INTOとDATA-GENこれら2つのRPG命令とYAJLライブラリーを併せて使うことで、RPGから簡単にWebサービスを使用できるようになります。
2020/04/06 ジョン・パリス、スーザン・ガントナー
私たちが、お客様からWebサービスに関する要望を受け取らずに1か月が過ぎることはほとんどないように思われます。お客様はWebサービスを使用または提供する任務を課され、JSONを生成および取り込む方法についてアドバイスを求め続けています。何年か前にはそのような要望はXMLに関係するものでしたが、今日ではもっと効率的な側面をもつJSONがほぼ完全に取って代りました。事実、私たちの一番最近の要望は、要求と応答の双方にJSONを使用するSOAP Webサービスに関係していました。これはSOAPがXMLをベースにしたプロトコルであるという点で少々普通ではありませんでした。
RPGによるJSONの生成と取り込み
もちろんこれを難しい方法で、つまり要求を一つながりにし、かつ応答を解析するのを「手作業」で行うことはいつでもできるでしょう。しかし、RPGは私たちに次の3つの主な代替策を提供します。
- SQLを組み込み、そのJSON機能を使用する
- スコット・クレメント氏のYAJLライブラリーのJSON生成および解析用APIを使用する
- RPG本来の機能であるDATA-GENとDATA-INTOをYAJLライブラリーが提供する機能と一緒に使用する
3番目の選択肢が、この記事の関心の中心になります。その理由は、それが一番柔軟性の高い解決法であると筆者は気付いたからです。読者の何人かは、私たちが以前YAJLの使用に関する記事を書いたことを思い出すでしょう。しかし、私たちが受け取り続けている質問を考慮して、以前の記事の情報をもっと纏まりのある「ハウツー」記事として集約するのが適切であると確信しました。
手近な課題
この記事の目的を達成するために、お客様向けの見積りシステムを作成し、見積りの中で使用される価格はWebサービスを使って読み取られるものと想定します。各Webサービス要求には1から50までの可変数の品目データが含まれています。
したがって、基本的な操作の流れは以下のようになります。
- お客様の見積りデータを集める
- JSON要求を生成する
- JSON要求をWebサービスに送出し、応答を受け取る
- 価格情報を取り出すために応答のJSONを解析する
- 見積り処理を完了する
要求の受け取り法や見積りの配信法に関係なく、行われる基本的な値付け操作は常に同じであるとします。
JSONの形式
もしJSONに馴染みがなければ、私たちの書いた「An RPGer’s First Steps With JSON」という記事をお読みください。
Webサービス要求に必要なJSONは次のような形式になっています。
{ "QuotationId": "A12345", "CustomerNumber":123456, "CustomerClass":"D2", "LineItems": [ { "ItemNumber":1, "Quantity":2 }, ... 必要なだけ品目を繰り返す ... ] }
要求の形式を整える
要求の形式を整えるのにRPGのDATA-GEN命令を使うつもりですが、この命令コードの構造に関心を抱く前に、JSONを作るためのデータ・ソースとして使われるデータ構造(以下DSと略記)を設計する必要があります。
このDSは、フィールド名およびそれらのフィールド間の関係の両方がJSONと合致していなければなりません。その理由は、それがDATA-GENが(そしてついでに言えば、XML-INTOやDATA-INTOが)動作する方法だからです!私たちが設計したDSを次に示します。
(A) dcl-ds Request qualified Inz; (B) QuotationId char(6); CustomerNumber char(6); CustomerClass char(2); (C) num_LineItems int(5); // カウントは要素数をコントロールする (D) dcl-ds LineItems Dim(50); ItemNumber char(10); Quantity int(5); end-ds LineItems; end-ds Request;
(A) (XMLと違い)JSONはルート要素に名前を付けないので、DS(要求)の名前は任意です。しかし、後で応答データを処理するために設計するDSの中で同じフィールド名を使用しなければならないので、これにQUALIFIEDを指定する必要があります。
(B) これがJSON内の要素を識別するのに使われるのと厳密に同じ名前です。それは厳密に一致していなければなりません。少し後でRPGフィールド名として許されていない文字を含んでいるJSONの取り扱い方法について話をします。フィールドの順序は重要ではありませんが、それらの階層上の位置(このケースでは、DS内の第1水準にあります)は重要です。
(C) このフィールドはオプションですが、これがないとLineItem DS配列にある50個の要素1つにつき1つのJSON配列を生成します ー しかもすべて空で!これはほとんどのWebサービスにとって歓迎できないことでしょう。このフィールドに値をセットすることで、DATA-GENに必要な数の要素だけを生成するよう指示します。DS配列とこの個数フィールドの間の関連は名前によって結び付けられます。すなわち、個数フィールドの名前の前には「num_」という接頭辞が付くことを除けば名前は同じであるということです。この関連付けがどのように行われるかは、DATA-GEN命令について見るときに説明します。
(D) JSONのLineItem要素は配列(これは[]で示されています)で構成されているので、ItemNumberとQuantityフィールドからなる入れ子状のDS配列としてコーディングされています。
このようなケースが、私たちがデータ構造を直接入れ子状にする能力を本当に高く評価する理由です。この能力のお陰でDSの形がJSONの形に確かに一致しているか大変容易に分ります。
ここで詳細すべてについて立ち入る時間はありません。これについては将来の記事で取り上げる予定ですが、もしDSがどのような形になっているべきか解明するのに難渋しているのなら、助けになるものがあります。スコット・クレメント氏は最近YAJLライブラリーにYAJLGENプログラムとそれに付随するコマンドを追加しました。このプログラムは単純なJSON文書を読み込んで関連するDSを生成します。このDSはDATA-GENまたはDATA-INTOのどちらかで使用できます。それは多くのことを推測しなければならないので完璧ではありませんが、取っ掛りとしては大いに助けになり得ます。
一旦DSが出来上がれば、JSON文を作るために必要なDATA-GEN命令をコーディングできます。
(E) dcl-s JSONRequestText varchar(4000) inz; (F) dcl-s itemCount int(5); // 見積もり内の品目の数 (G) // 要求DSに要求品目をロードするための仕分けロジック Request.num_LineItems = itemCount; // 要求DSに品目数をセット (H) Data-Gen Request %Data( JSONRequestText : 'countprefix=num_' ) %Gen('YAJL/YAJLDTAGEN');
(E) これはJSON文を保持する変数です。その内容は後でWebサービスに渡されます。私たちの簡単な例では4,000バイトで十分です ー あなたの用途に合うようにサイズを調整してください。
(F) 見積りの予備処理はこのカウンターの値を増加させます。Request.num_LineItemsdirectly内にカウントを構築することももちろんできましたが、そのロジックを示していないので、これはDATA-GENを呼び出す前にカウントを設定しなければならないということを明確にする良い方法のように思われます。
(G) RPGロジックが要求DSの内容を設定するのはここになります。
(H) DATA-GENは生成されたデータ(要求)を受け取るためのフィールドを識別することから始めます。%DATA組み込み関数は生成されたデータのソースとなるDSを識別すると共に、カウント・フィールドを識別するための接頭辞として使われる「num_」という文字列を指定します。大事なことを言い忘れていましたが、生成プロセスを実行するプログラムまたはプロシージャを指定します。この例題の場合、それはスコット氏のYAJLライブラリーの一部として提供されているYAJLDTAGENというプログラムになります。
注:この特別なプログラムは比較的最近YAJLライブラリーに追加されたので、もしあなたのシステム上にこれが見当たらなければ、それは単に更新版を導入する必要があるということを意味しています。
現時点で私たちは要求用のJSONを作り、使用する送信の仕組みとして何を使うかには関係なく単にそれをWebサービスに送出する必要があります。次にWebサービスはJSON応答を返し、その結果を処理する前にそれをRPGで処理できる形に変換する必要があります。それはDATA-INTOの仕事です。
結果を処理する
DATA-GENで処理を行ったのと同じように、私たちが最初に行う必要があるのは、デコードされたJSONを保持するDSを設計することです。この場合、それは基本的に要求DSをコピーすること、それを応答に名称変更すること、そして追加の3つの価格フィールドを加えることでした。結果は下記の通りです。
dcl-ds Response qualified; QuotationId char(6); CustomerNumber char(6); (I) num_LineItems int(5); // 発見された要素の数 dcl-ds LineItems Dim(50); ItemNumber char(15); Quantity int(5); UnitPrice packed(9:3); UnitDiscount packed(9:3); ExtendedPrice packed(15:2); end-ds LineItems; end-ds Response;
以前行ったようにDSにカウント・フィールドを含めていることに注意してください。しかし、JSONには対応する値はありません。JSON中に見つけたLineItems要素の数に基づいてDATA-INTOがこの値を書き込みます。 DSを作り終えたら、最後に行うことはWebサービス応答をDSに書き込むためにDATA-INTO命令をコーディングすることです。
Data-Into Response %Data( jsonResponse : 'countprefix=num_ case=any' ) %Parser('YAJL/YAJLINTO');
次のオペランドは、Webサービスの応答データ(jsonResponse)が入っているフィールドの名前と適用する処理オプションを提供する%DATA組み込み関数です。カウント・フィールドを識別するための接頭辞オプションは既に見ましたが、case=anyについてはXML-INTOに不案内な人のために少し説明が必要です。基本的にその目的は、RPGのフィールド名が大文字と小文字の区別をしないという事実に対処することです。システム内部ではコンパイラーはabc、Abc、ABcというフィールド名をすべてABCという名前として扱います。これは、DATA-INTOが解析プログラムによって識別される品目名と対応するフィールド名とを一致させようとするときに問題になります。私たちの例題の場合、解析プログラムはQuotationIdやCustomerNumberのような名前を返しますが、RPGはDS内の同じフィールドをQUOTATIONIDおよびCUSTOMERNUMBERとして認識しており、結果的に名前が一致しません。case=anyは、比較を行う前に解析プログラムから受け取った名前をすべて大文字に変換するようDATA-INTOに指示することでこの問題に対処します。言い換えれば、ほとんど常にこのオプションを指定する必要があります。
最後のオペランドは%Parser組み込み関数で、これは文書を解析するプログラムまたはサブプロシージャを識別します。IBMは手本としてJSON解析プログラムを提供してはいますが、実際のところこれは教育目的のもので「実用版」ではありません。私たちはYAJLライブラリーのYAJLINTOプログラムを使いました。これは大変良く機能しかつ非常に高速です。
行うべきことはこれですべてです。JSON応答からの全データは応答DSの中に分解されて入れられ、処理できる状態にあります。結果のデータをアクセスする方法を説明する、私たちが使用したサンプル・プログラムの一部を下に示します。もしこのプログラムを弄ってその動きを研究したいのなら、プログラムをここからダウンロードしてください。
Dsply ( 'Processed ' + %char(Response.num_LineItems) + ' items - details:' ); // Process all Line Items returned ... For i = 1 to Response.num_LineItems; Dsply ( ' Item: ' + Response.LineItems(i).ItemNumber ); Dsply ( ' Price: ' + %Char(Response.LineItems(i).UnitPrice) + ' Discount: ' + %Char(Response.LineItems(i).UnitDiscount) ); EndFor;
基本を超えて
この記事を書き始めてすぐに、言い足りないことが沢山あることが明らかになりました。皆さんが抱えている数多くのJSON処理ニーズに対処するために、この記事がこれら2つの命令を組み合わせて使うための基本事項への良い入門となることを願います。しかし、私たちがこの記事で取り上げられる以上の例外的な状況が常に存在します。とは言いながら、お客様との議論で何度も話題になった1つの質問に簡単に触れようと思います。その質問は「JSON文書の一部を処理する必要が出たらどうするか?」というものです。
もちろんこれに対処する方法は色々あり、選択するべき最善の方法はJSONの形式およびあなたの抱える固有の要件に依存します。一例を差し上げましょう - 処理速度を向上させるためにWebサービス応答の最初の部分を無視したいとしましょう。データが送信したデータと厳密に同じでなければならないとまず考えるのが妥当です。 以下に、これを遂行するために使うことができるデータ定義と改訂されたDATA-INTOを示します。
(J) dcl-ds Psds PSDS; elementCount int(20) Pos(372); end-Ds; dcl-ds LineItems Qualified Dim(50); ItemNumber char(15); Quantity int(5); UnitPrice packed(9:3); UnitDiscount packed(9:3); ExtendedPrice packed(15:2); end-ds LineItems; Data-Into LineItems (K) %Data( jsonResponse : 'path=jsonData/LineItems case=any' ) (L) %Parser('YAJL/YAJLINTO': '{ "document_name" : "jsonData" }' );
(J) DATA-INTO命令の対象は配列なので、RPGは自動的にPSDSの372桁目にある要素数の値を保持します。
(K) ここで、処理を始める前にLineItemエントリーの開始位置までドリルダウンするようDATA-INTO命令に指示するためにpath=オプションを使いました。しかしこれには問題があります。JSON文書にはXML文書のような外側の要素に関連する名前がありません。この例では、私たちはこれに対処するためにjsonDataという独自のルート名を考え出しました。しかしそれに続いて(L)のようなコーディングが必要になります。
(L) 解析プログラムYAJLINTOにその名前がどのようなものであるべきかを指示する必要が有ります。これは組み込み関数%Parserの第2パラメータを通じて指定されます。このオプションそれ自身はJSONテキストの形式になっています。この例題の場合、document_nameオプションだけを使用しているのでJSONはとても単純です。
まとめ
これですべてです。この記事により、このプロセスがどのように連携するのか良く理解できることを願います。しかし申し上げたように、あなたが直面するかもしれない他の多くの状況があり、次の記事でそれらについて取り上げる予定です。それまでの間、この領域で何か緊急の援助が必要なことがあればcontact@Partner400.com宛に私たちに連絡をください。