これは Delphi Advent Calendar 2017 21日目の記事です。
2017年12月13日にリリースされた Delphi/C++Builder 10.2.2 では Enterprise版とArchitect版に RAD Server の配置ライセンスが含まれますが、まずは開発環境で使ってみましょう
RAD Server は Delphi/C++Builder Enterprise 版以上で REST JSON な API を実装するときの選択肢の一つでしたが、これまでは実運用時に配置ライセンスが必要でしたので、便利だなーと思いつつも利用に踏み切れず、DataSnap を用いているケースは案外多いのではないでしょうか。
しかし2017年12月13日にリリースされた10.2.2からは Enterprise 版、Architect版に RAD Server の配置ライセンスが含まれる(正確にいうと、Enterprise, Architect のユーザ向けに RAD Server シングルサイト配置ライセンスが1ライセンスが発行される)ことになりましたので、自社向けのシステムでは今後は RAD Server を選びやすくなったかもしれません。
そんな場合に「RAD Server での REST API ってどうやって作るんだ?」とか「レスポンスのJSONはどうやって作るんだ?」という疑問が出て来ると思います。
そこでこの記事では 10.2.2 で RAD Server を用いて REST API を実装し、JSON で返すパターンを3種類やってみます。
なお、RAD Server では管理データベースとして InterBase XE7 を使用します。開発環境に InterBase XE7 をセットアップしていない場合は事前に追加インストールしておいてください。
※2018年3月にリリースされた RAD Studio/C++Builder 10.2 Tokyo Update 3 でC++向けの RAD Server プロジェクトが作成できない場合は下記のパッチを適用してください。
https://cc.embarcadero.com/item/30832
作業に入る前に、RAD Server と InterBase の関係について
RAD Server では、下記ような機能のために内部データベースに InterBase を使用します。
- ユーザ管理、ユーザ認証
- グループ管理
- 接続クライアント管理
Delphi/C++Builder には開発版の InterBase(Developer Edition)が利用できますので、今回の内容を開発環境で実行する場合は、InterBase Developer Edition をインストールしておいてください。
基本的にはプロジェクトウィザードに任せるだけで REST API の雛形は完成。
REST でリクエストを受ける部分は RAD Server 側がすべてやってくれます。基本的にはウィザードにおまかせするだけです。
ではウィザードを使ってプロジェクトの雛形を開くところをやってみます。
新規作成のポップアップウィドウにて「Delphi プロジェクト」または「C++Builder プロジェクト」の「EMS」「EMSパッケージ」の順に選択して「OK」をクリックします。
RAD Server なのにEMSという名称のプロジェクトを選んでいるわけですが、そもそもRAD ServerのREST API機能は以前はEnterprise Mobility Serviceという名称の機能で提供していました。そしてEMSという名称がついた機能は現在のバージョンでもそのまま用いられています。
さて、先に進むことにしましょう。次に表示されるウィザードでは「リソースを含むパッケージを作成する」を選んで次に進みます。
リソース名に任意の文字列を指定します。このリソース名に指定した文字列は実際に実装する REST API の URL に用いられます。この例だと開発時のAPIのURLは http://localhost:8080/senchaRAD/ となります。
ウィザードの最終段階に来ました。ここでは実装するエンドポイントを選択します。デフォルトでは Get, GetItem が選択されています。それぞれのエンドポイントが Get, Post, Put, Delete の文字列で始まることから想像が付くと思いますが、これらはそれぞれのメソッドに対応したエンドポイントです。
なお、C++Builder を選んだ場合はウィザードが生成した雛形の内容がDelphiに比べて少し不親切な状態となっています。これでも本記事の内容には特に影響ありませんが、同等の状態で開始したい場合は下記リンク先の記事を参考に TEMSResource1::Get()
や TEMSResource1::GetItem()
の内容を修正してください。
https://qiita.com/kazinoue/items/d673d2a202ba0d071638
実際に実装してみよう
その1:TJSONArrayやTJSONObjectを操作してJSONを作る
ウィザードが終了すると空っぽのデータモジュールが表示されていると思いますが、ここに対して4つのコンポーネントを配置します。ただしポトペタの作業は3つです。
このスクリーンショットは、IDE右上のペインからデータエクスプローラを選択して"FireDAC"=>InterBase=>EMPLOYEE=>EMPLOYEEテーブルを選び、ドラッグ&ドロップしています。するとFDConnectionとFDQueryの2つがフォーム上に配置されます。この2つはInterBaseのEmployeeデータベース内のEmployeeテーブルを参照する設定がすでに入っている状態です。
さらに続けて次の2つを右下のツールパレットからフォームに配置します。これらはInterBaseのドライバや待機カーソルです。開発時には配置していなくとも一応動作しますが、実運用時にはこれらが必要です。
TFDGUIxWaitCursor
TFDPhysIBDriverLink
コンポーネントの配置が終わったら、TSenchaRADResource1.Get のプロシージャを以下のように書き換えます。これは InterBase に対して SELECT クエリーを実行し、戻されたレコードセットをJSON配列で返す、というコードの実装例です。while ループでクエリの結果を1レコードづつ取得して TJSONArray を作っています。それぞれのカラムはすべて文字型で扱っていますが、JSONでは文字と数値は区別可能ですから実際の実装では適切に区別するほうがベターです。
procedure TSenchaRADResource1.Get(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse);
var
empJSONResponse: TJSONObject;
empJSONArray: TJSONArray;
empJSONRecord: TJSONObject;
aField: TField;
count: Integer;
begin
empJSONResponse := TJSONObject.Create;
empJSONArray := TJSONArray.Create;
count := 0;
// クエリの実行結果を1件づつ取得してJSONArrayに追加する
EmployeeTable.Open;
while (not EmployeeTable.Eof) do
begin
empJSONRecord := TJSONObject.Create;
for aField in EmployeeTable.Fields do
begin
empJSONRecord.AddPair(LowerCase(aField.FieldName),aField.AsString);
end;
empJSONArray.Add(empJSONRecord);
EmployeeTable.Next;
inc(count);
end;
empJSONResponse.AddPair('employee',empJSONArray);
AResponse.Body.SetValue(empJSONResponse,True);
end;
void TSenchaRADResource1::Get(TEndpointContext* AContext, TEndpointRequest* ARequest, TEndpointResponse* AResponse)
{
TJSONObject* empJSONResponse = new TJSONObject();
TJSONArray* empJSONArray = new TJSONArray();
int count = 0;
TField* aField;
// クエリの実行結果を1件づつ取得してJSONArrayに追加する
EmployeeTable->Open();
while ( !EmployeeTable->Eof ) {
TJSONObject* empJSONRecord = new TJSONObject();
for (int i = 0; i < EmployeeTable->FieldDefs->Count; i++) {
aField = EmployeeTable->FieldByName(EmployeeTable->FieldDefs->Items[i]->Name);
empJSONRecord->AddPair(LowerCase(aField->FieldName),aField->AsString);
}
empJSONArray->Add(empJSONRecord);
EmployeeTable->Next();
count++;
}
empJSONResponse->AddPair("employee",empJSONArray);
AResponse->Body->SetValue(empJSONResponse,True);
}
初回のビルドの際の注意(Delphi)
ではこれをビルド、実行してみましょう。Delphi で EMSパッケージの新規プロジェクトを最初にビルドするときはこのようなポップアップが表示されますが、OKを押して先に進んでください。必要なパッケージの参照が追加された上でビルドが行われます。
初回のビルドの際の注意(C++Builder)
C++Builder ではそのままビルドすると上記の画面が表示される代わりに次のようなエラーが出ているはずです。
C++Builderの場合はパッケージの参照をご自身で追加して頂く必要があります。この方法は「プロジェクトの Requires に参照を追加する」か「コードに #pragma link を追加する」というやり方がありますが、ここでは Requires に追加する方法を紹介します。
IDE画面右上のプロジェクトマネージャで Requires を右クリックして「参照の追加」を行ってください。
C:\Program Files (x86)\Embarcadero\Studio\19.0\lib\win32\release 内の下記の *.bpi を追加します。
- dbrtl.bpi
- FireDAC.bpi
- FireDACCommon.bpi
- FireDACCommonDriver.bpi
- FireDACIBDriver.bpi
この作業を行った結果、Requires が以下のとおりであれば、再度ビルドしてみてください。今度はビルドが通るはずです。
コードに #pragma link を記述する場合は以下のドキュメントをご参照ください。
http://docwiki.embarcadero.com/RADStudio/Tokyo/ja/Pragma_link
EMS開発サーバを初めて実行する時の注意
無事にビルドできたら「EMS開発サーバ」が起動してくるはずです。なお、EMS開発サーバを初めて実行する場合もウィザードが走ります。RAD Server(EMS) はユーザ認証等の内部データをInterBaseで管理しますが、このウィザードではRAD Server(EMS)向けのデータベースがセットアップされます。基本的には単に先に進むだけでOKなのですが、参考までにスクリーンショットを貼っておきます。
「EMSライセンスを持たない InterBase インスタンスインスタンスを使用」云々と表示されますが、これはDelphi/C++Builderの開発環境にインストールしたInterBaseに対して RAD Server(EMS) の内部データベースをセットアップする際に表示されるワーニングです。ここでは実際にそのような環境をセットアップしているのですから、特に気にせずに先に進みます。
この画面が出たら構成ウィザードは終了ですので OK を押します。
ウィザードが完了すると RAD Server (EMS) が起動し、以下のような表示となっているはずです。
RAD Server に接続してみる
ではこのRAD Serverに対して接続してみることにしましょう。ブラウザを起動して http://localhost:8080/senchaRAD/ のように入力します。リソース名を別の名前に設定した方は senchaRAD の部分を実際のリソース名に置き換えてください。
するとブラウザにはこのような表示が出ていると思います。(なお、通常は JSON を整形せずに表示しているはずです。この表示は Google Chrome に対して JSON View という拡張機能を追加することで整形表示させています)
JSONの形式をすこし変えてみます
ではEMS開発サーバを一旦止めて、さらに JSON の形式をすこしだけ変えてみましょう。TSenchaRADResource1.Get の最後の2行を以下の様に変更します。元々の実装は JSONArray に対して 'employee' というキーとペアにしていますが、書き換え後の処理は JSONArray をそのまま返しています。
// empJSONResponse.AddPair('employee',empJSONArray);
// AResponse.Body.SetValue(empJSONResponse,True);
AResponse.Body.SetValue(empJSONArray,True);
end;
// empJSONResponse->AddPair("employee",empJSONArray);
// AResponse->Body->SetValue(empJSONResponse,True);
AResponse->Body->SetValue(empJSONArray,True);
}
これをビルドしてAPIに再度アクセスしてみると、今度はこのような形のデータが取得できているはずです。これはルート要素のないJSON配列になっています。
どちらの形式を用いるにせよ、これは一般的によくありがちな JSON 形式ですね。任意の JSON パーサーでご利用いただけます。Delphi/C++Builder でネイティブアプリから使う場合は RESTデバッガを用いると接続確認ができますし、確認できたAPIの利用に必要なコンポーネントや設定一式を RESTデバッガからコピー&ペースト操作でIDEのデザインフォームに貼り付けることができます。
また、もちろん Sencha ExtJS や Architect で利用いただいてもOKです。
さて、ここまでの状態が出来上がったら、プロジェクトを一度保存しておいてください。ここで保存したプロジェクトは、その2、その3のベースとして使います。
その2:TFDSchemaAdapter と TFDStanStorageJSONLink を使ってみる
さきほどの実装に対して、コンポーネント2つを追加します。TFDSchemaAdapter と TFDStanStorageJSONLink です。
この2つのコンポーネントはいずれも FireDAC のコンポーネントですが、JSON の生成のためのコードをほとんど書かずにすみます。ただし出力されるデータフォーマットは FireDAC の内部形式となり、少々冗長な出力です。
配置したコンポーネントのうち FDSchemaAdapter1 (TFDSchemaAdapter) は EmployeeTable (TFDQuery) の SchemaAdapter プロパティに紐付けておきます。
ではコードを修正しましょう。最初のコードとは違って随分短いコードです。
procedure TSenchaRADResource1.Get(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse);
var
oStr: TMemoryStream;
begin
oStr := TMemoryStream.Create;
// クエリの実行結果をスキーマアダプタからメモリストリーム経由で返す
EmployeeTable.Open;
FDSchemaAdapter1.SaveToStream(oStr,TFDStorageFormat.sfJSON);
AResponse.Body.SetStream(oStr,'application/json', True);
end;
void TSenchaRADResource1::Get(TEndpointContext* AContext, TEndpointRequest* ARequest, TEndpointResponse* AResponse)
{
TMemoryStream* oStr = new TMemoryStream();
// クエリの実行結果をスキーマアダプタからメモリストリーム経由で返す
EmployeeTable->Open();
FDSchemaAdapter1->SaveToStream(oStr,TFDStorageFormat::sfJSON);
AResponse->Body->SetStream(oStr, "application/json", True);
}
このコードが行っていることは FDQuery でクエリを実行しつつ、その結果を TFDSchemaAdapter で TMemoryStream に JSON 形式で保存し、それを HTTPレスポンスの Body として返す、という処理です。
そして返されるJSONは先程とは異なっていて構造が冗長になっています。テーブルのそれぞれのカラムの詳細な情報が受け渡されています。この中で RowList のキーを赤くマーキングしていますが、このJSON配列に欲しいデータが格納されています。このJSON配列を参照する場合のルート要素は FDBS.Manager.TableList[0].RowList となります。
なお、ここで紹介した方法は JSON でとりあえず返すところまでを行っていますが、下記のチュートリアルではもう少し細かい作業を行いつつ、RAD Server 側の実装とクライアント側の実装を行っています。この方法で実装したサーバ、クライアントについては REST JSON ということを全く意識せずにクライアントからWeb API経由でデータベース操作できます。
Delphi/C++Builder開発者の方はこちらも併せてお読み頂くとよいでしょう。
http://docwiki.embarcadero.com/RADStudio/Tokyo/ja/チュートリアル:FireDAC_EMS_リソースを実装する
http://docwiki.embarcadero.com/RADStudio/Tokyo/ja/チュートリアル:FireDAC_EMS_クライアント_アプリケーションを実装する
Ext JS で利用する場合はマッピングの調整が必要になるものの、閲覧は特に不自由なく行えます。
その3:Delphi/C++Builder 10.2.2 で TFDBatchMove に追加された新しい方法を使ってみる
3つめの方法は TFDBatchMoveJSONWriter を使う方法です。この方法は 10.2.2 から利用可能です。これより前のバージョンをお持ちの方はお試し頂けません。
では早速やってみましょう。その1で保存したプロジェクトに対して、3つのコンポーネントを追加します。
TFDBatchMoveDataSetReader
TFDBatchMoveJSONWriter
TFDBatchMove
TFDBatchMoveDataSetReader の DataSet プロパティに FDQuery( EmployeeTable ) を設定しておきます。
また、TFDBatchMove の Reader, Writer には今回配置した TFDBatchMoveDataSetReader と TFDBatchMoveJSONWriter を指定します。
そして TSenchaRADResource1.Get のプロシージャを以下のように書き換えてみます。
procedure TSenchaRADResource1.Get(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse);
begin
// JSONWriter の書き出し先を設定して BatchMove を実行
FDBatchMoveJSONWriter1.JsonWriter := AResponse.Body.JSONWriter;
FDBatchMove1.Execute;
end;
void TSenchaRADResource1::Get(TEndpointContext* AContext, TEndpointRequest* ARequest, TEndpointResponse* AResponse)
{
// JSONWriter の書き出し先を設定して BatchMove を実行
FDBatchMoveJSONWriter1->JsonWriter = AResponse->Body->JSONWriter;
FDBatchMove1->Execute();
}
コンポーネントは3つ配置しましたが、コードは2行だけです。ではビルド・実行してブラウザでアクセスしてみましょう。
返された結果は、その1でJSONArrayをそのまま返すようにした状態と同様にルート要素のないJSON配列の形式です。従って、その1と同様、JavaScript 実装でも、Delphi/C++Builder でも利用できます。
しかしその1の結果にくらべると 数値や日付の形式が違っています。その1のコードでは全ての値を String 扱いにしていましたが、TFDBatchMoveJSONWriter ではデータ型に合わせた形式でJSONを出力しています。こちらのほうが、データ型をより厳密に考慮した出力を行っているわけですね。
ではこれにルート要素名を付けることにしましょう。先程の2行のコードのかわりに、こんなふうにしてみます。
procedure TSenchaRADResource1.Get(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse);
var
empJSONRseponse: TJSONObject;
empJSONArray: TJSONArray;
begin
// 最終的な出力のための JSONObect と、その中に入れる JSON配列を初期化する
empJSONRseponse := TJSONObject.Create;
empJSONArray := TJSONArray.Create;
// FDBatchMoveJSONWriter の書き出し先を設定して処理を実行する
FDBatchMoveJSONWriter1.JsonArray := empJSONArray;
FDBatchMove1.Execute;
// ルート要素名を employee にしてレスポンスを返す
empJSONRseponse.AddPair('employee',FDBatchMoveJSONWriter1.JsonArray);
AResponse.Body.SetValue(empJSONRseponse,True);
// FDBatchMoveJSONWriter1.JsonWriter := AResponse.Body.JSONWriter;
// FDBatchMove1.Execute;
end;
void TSenchaRADResource1::Get(TEndpointContext* AContext, TEndpointRequest* ARequest, TEndpointResponse* AResponse)
{
// 最終的な出力のための JSONObect と、その中に入れる JSON配列を初期化する
TJSONObject* empJSONResponse = new TJSONObject();
TJSONArray* empJSONArray = new TJSONArray();
// FDBatchMoveJSONWriter の書き出し先を設定して処理を実行する
FDBatchMoveJSONWriter1->JsonArray = empJSONArray;
FDBatchMove1->Execute();
// ルート要素名を employee にしてレスポンスを返す
empJSONResponse->AddPair("employee",FDBatchMoveJSONWriter1->JsonArray);
AResponse->Body->SetValue(empJSONResponse,True);
// FDBatchMoveJSONWriter1->JsonWriter = AResponse->Body->JSONWriter;
// FDBatchMove1->Execute();
}
では実際にアクセスして確認します。すると、こんなふうにルート要素名が入る形でレスポンスを得ることができました。
というわけで、3種類のやりかたで RAD Server から JSON を返す API を作成してみました。
なお、Sencha ExtJS と組み合わせる場合は、その1、またはその3の方法が便利です。その2は、クライアントーサーバ型のシステムを3層化つつ、クライアント側でもネイティブアプリだけを引き続き利用するような場合に向いています。
RAD Server の配置ライセンス、どこで使っていますでしょうか? これは開発環境なので配置ライセンスは使っておりません。
この手順では、実は配置ライセンスは適用しておりません。配置ライセンスはあくまで本番環境構築時のアクティベーションで使うキーです。ビルドしたアプリを開発環境で実行する分には配置ライセンスは不要であり、Enterprise以上の開発環境があれば良いのです。
Appendix