Uncategorized

datajs で OData をフル活用する

環境 : Visual Studio 2010, jQuery 1.5.1, datajs 1.0.2

こんにちは。

これまでの WCF Web Api では “作る側” を紹介しましたが、今回は、使う側 (Consumer) を紹介したいと思います。エバンジェリスト 井上 (大輔) も以前 記載していた datajs について紹介します。

datajs は、OData サービス (Web Api) を扱える JavaScript ライブラリーです。
しかし、OData は RESTful なサービスなので、ブラウザー (JavaScript) から使用するなら jquery の jQuery.ajax ($.ajax)、jQuery.getJSON でも充分です。では、datajs を使う意義は何でしょうか ? (決して、jQuery とケンカしてしまうようなライブラリーではありません。jQuery との親和性もバッチリです !)
そこで、まず基本的な使い方を紹介し、後半で、こうした datajs を使うメリットについて紹介したいと思います。

 

datajs の基礎

まずは、基本的な使い方についてです。

OData のサービスに接続 (データを取得) する際は、井上も記載している通り、OData.read か OData.request を使います。

  • GET を使った基本的な操作は OData.read を使うと便利です
  • その他のメソッド (POST / PUT / MERGE / DELETE) や、より複雑なリクエストのカスタマイズでは、Low level API である OData.request を使うことができます

補足 : なお、OData.read でも、ヘッダーなど (Accept ヘッダー、Cache コントロール、など)、リクエストの簡単なカスタマイズは可能です。

実際、OData.read は、内部で、OData.request を呼び出しています。
いずれも、第一引数には、下記の構成の Request オブジェクト (下記の JSON オブジェクト) を渡しますが、OData.read では URI の文字列をそのまま渡すこともできます。

{ requestUri: "http://myserver/test.svc/Datas" }
{  requestUri: "http://myserver/test.svc/Datas",  method: "GET"}
{  requestUri: "http://myserver/test.svc/Datas",  method: "POST"  data: { ...省略 }}

例えば、下記の通り記述すると、/DatajsSample/TestService.svc/Customers の OData サービス (Web Api) からデータを取得して、table に結果の一覧を表示します。(データは、内部で json フォーマットが使用されます。)

. . .<table border="1">    <thead>        <tr>            <th>                Id            </th>            <th>                Name            </th>        </tr>    </thead>    <script id="template1" type="text/x-jquery-tmpl">        <tr>            <td>                ${CustomerId}            </td>            <td>                ${Name}            </td>        </tr>    </script>    <tbody id="tableData1">    </tbody></table><script type="text/javascript">    function loadData() {        $('#tableData1').empty();        OData.read('/DatajsSample/TestService.svc/Customers',            function (data) {                $("#template1").tmpl(data.results).appendTo("#tableData1");            },            function (err) {                alert(err.message);            });    }    $(document).ready(loadData);</script>. . .

補足 : 上記を動かすには、あらかじめ、CodePlex から datajs-1.0.2.min.js をダウンロードして、Script 参照 (src) を追加しておいてください。(ついこの前まで、バージョン 1.0.1 でしたが、頻繁にバージョン アップしているみたいです。。。)
また、上記では、jquery の Templates プラグインも使用しています。

補足 : エラー「プロパティ parse の値を取得できません: オブジェクトは Null または未定義です。」が表示される場合は、http://cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js へのスクリプト参照を追加してください。
こちら に記載した通り、Internet Explorer 7 以前や、Internet Explorer 8 以上でも Compatibility Mode で実施されている場合、JSON オブジェクト (JSON.parse、JSON.stringify などで使用) が参照できず、このエラーが発生します。

もちろん、データの更新も可能です。(ここでは省略しますが、井上 大輔のブログ を参照してください。)

また、下記の通り、JSONP に対応したクロス ドメインの呼び出しも、1 行の追加で可能です。

補足 : 下記を動作させる前に、クロス ドメイン呼び出しが可能 ($callback が使用可能) になるように、以前、「ブラウザーからのクロス ドメイン接続 (JSONP) と SSL」 に記載した方法で、サービス側 (WCF Data Services など) を構成しておいてください。

. . .<script type="text/javascript">    OData.defaultHttpClient.enableJsonpCallback = true;    function loadData() {        $('#tableData1').empty();        // 別のドメインのサービスを呼び出し        OData.read('http://other_domain/DatajsSample/TestService.svc/Customers',            function (data) {                $("#template1").tmpl(data.results).appendTo("#tableData1");            },            function (err) {                alert(err.message);            });    }    $(document).ready(loadData);</script>. . .

また、OData.read, OData.request では、Basic 認証 (Basic authentication) を使用して OData サービス (Web Api) に ID / パスワードを渡して処理することもできます。(ただし、jsonp の場合は、もちろん、不可能です。)

また、OData 特有の $select、$filter などのパラメーターは、基本的に URI でそのまま渡します。(例えば、http://myserver/test.svc/Datas?$top=3 といった具合です。) 例えば、OData では、$metadata を使って、エンティティのメタ情報 (フィールド名、型、など) を取得できるため、下記の通り記述すると、エンティティ名 (Cutomers など)、プロパティ名 (CustomerId など)、プロパティの型 (Edm.String, Edm.Int など) の情報を表示できます。

function getMetadata() {    OData.read('/DatajsSample/TestService.svc/$metadata',    function (data) {        $.each(data.dataServices.schema[0].entityType, function (i1, val1) {            alert('Entity = ' + val1.name);            $.each(data.dataServices.schema[0].entityType[i1].property, function (i2, val2) {                alert(val2.name + ' is ' + val2.type);            });        });    }, function (err) {        alert('failure');        alert(err.message);    }, OData.metadataHandler);}

また、リレーションも、同様に、$expand、$links などを使って処理できます。

補足 : 例えば、Orders と Customers の間にリレーションが張られている場合、http://myserver/DatajsSample/TestService.svc/Orders?$expand=Customers のような形式で Orders 以下の Customers が展開できます。また、Orders の 3 番目のエンティティと関連している Customers の Uri を取得する場合は、http://myserver/DatajsSample/TestService.svc/Orders(3)/$links/Customers で取得できます。

 

Batch 処理

と、ここまでなら、上述の通り、jquery を使えば充分でしょう。(わざわざ、こんなマイナーなライブラリーを使う必要はありません。)
ここからは、OData が持つさまざまな仕様を datajs を使ってフル活用してみます。

まず、OData プロトコルでは、MIME のマルチパート (multipart) を使用して、複数の要求 (Request) を単一の HTTP リクエストで処理できる Batch と呼ばれる要求が可能です。(同様に、Response も Multipart で返ってきます。)

補足 : Batch リクエストは、ラウンド トリップを減らす目的で使用できますが、必ずしも、トランザクションを扱うものではあません。Batch でトランザクションを扱えるようにするかどうかは、サービスの実装に依存します。(例えば、Batch の 1 番目の更新要求に成功し、2 番目の更新要求に失敗しても、1 番目の要求がロールバックされるどうかはサービスの実装に依存します。)
なお、WCF Data Services では、開発者が、サービス側の DataServiceProcessingPipeline.ProcessingChangeset イベント、DataServiceProcessingPipeline.ProcessedChangeset イベントを処理することで、トランザクション処理を実装することができるようです。(すみません、私自身、そういうコードを実装した経験はないですが。。。)

補足 : 同様に、OData の仕様では、Batch 処理時の実行順序についても、サービスの実装に依存します。(必ずしも、並べた順番で処理が実行されるとは限りません。順序実行、並行実行などは、サービスの実装に依存します。)

この OData の Batch 要求も、下記の通り、datajs を使って簡単に処理できます。(下記では、Customer と Orders の双方のエンティティを 1 回の Request で更新しています。)

function changeData() {    OData.request({        requestUri: '/DatajsSample/TestService.svc/$batch',        method: 'POST',        data: { __batchRequests: [                { __changeRequests: [                    { requestUri: 'Customers(1)', method: 'MERGE', data: { Name: '日本マイクロソフト'} },                    { requestUri: 'Orders(2)', method: 'MERGE', data: { Name: '日本マイクロソフトからの発注', Price: 2000000} }                ]                }            ]        }    }, function (data, response) {        alert('success');        $.each(data.__batchResponses[0].__changeResponses, function(i, val) {            alert('Status Code : ' + val.statusCode);        });    }, function (err) {        alert('failure');        alert(err.message);    }, OData.batchHandler);}

補足 : 上記のコールバック関数の引数 response には、この Multipart の要求自体のレスポンス (全体の結果) が入っており、引数 data には、Multipart のそれぞれの結果が入っています。(なお、response.data には、引数 data と同じ結果が入っています。)
Multipart の処理自体 (全体) は成功しても、それぞれの処理でエラーとなっている可能性があるため、response.statusCode の確認だけでなく、それぞれの data.__batchResponses[0].__changeResponses[i].statusCode (i = 0, 1, …) の結果も確認しておきましょう。

 

ページング (Paging) とプリフェッチ (Prefetch)

レコード形式のデータを扱ってページングの機能を実現する場合、例えば、jQuery の dataTables プラグインなどを使ってページングを実現できますが、この場合でも、データ量が大量なケースでは、サーバー サイドの実装と連携することも考慮しなければなりません。
OData の場合は、こうしたサーバー サイドのページングを実現する際に、$top、$skip などを使って、「データの X 番目から Y 番目を取得」といったことが可能ですが、datajs ライブラリー (及び、jQuery の Deferred) と組み合わせることで、こうしたサーバー サイドの仕組みと連携した動作を、エレガントに実装できます。

まず、datajs.createDataCache 関数を実行すると、データのフェッチや保管の処理をおこなう管理オブジェクト (cache オブジェクト) が生成されます。
この際、初期化の引数として、json 形式で、データを取得する先の URI、ブラウザーに保管しておく最大キャッシュ サイズ (byte 数)、あらかじめプリフェッチをおこなうサイズ (データ件数) などを指定できます。(使用可能なプロパティの詳細については、こちら のドキュメントを参照してください。)
なお、データをブラウザー側に保持しておく方法 (メカニズム) については、以下の 3 種類から指定可能です。何も指定しないと、既定で、DOM Storage が使用できるブラウザーでは DOM Storage が使用され、使用できないブラウザーではインメモリー (In-memory) が使用されます。(Indexed DB は、まだ実装していないブラウザーがほとんどだと思うので、実験的に用意されています。大量データを扱う場合に向いています。)

  • In-memory (インメモリー)
  • DOM Storage
  • IndexedDB Storage

つぎに、作成 (初期化) された cache オブジェクトの readRange 関数を実行して、指定された範囲のデータを (OData サービスから) 取得できます。
この際、データの取得を同期的におこなって結果を処理するのではなく、jQuery の Deferred を使用して、非同期に (バック グラウンドに) 処理をおこなって、データが揃った段階で指定された処理を実行できます。(予約できます。)

実際のコードを見てみましょう。
例えば、以下のサンプル コードでは、ページのロードと共に、非同期で 300 件のデータが読み込まれ、ページ (HTML) 上にはそのうちの最初の 10 件が表示されます。内部では、10 件ずつ、合計 30 回の呼び出しが非同期におこなわれ、最初の 10 件が揃った段階でデータが表示されます。(下記の通り、readRange 関数の戻り値として Deffred オブジェクト (promises) が返ってくるので、then で処理を定義しています。)

. . .<table border="1">    <thead>        <tr>            <th>                Id            </th>            <th>                Name            </th>            <th>                Price            </th>        </tr>    </thead>    <script id="template1" type="text/x-jquery-tmpl">        <tr>            <td>                ${OrderId}            </td>            <td>                ${Name}            </td>            <td>                ${Price}            </td>        </tr>    </script>    <tbody id="tableData1">    </tbody></table><script type="text/javascript">    var opt = { name: "testCache", source: '/DatajsSample/TestService.svc/Orders', pageSize: 10, prefetchSize: 300, cacheSize: -1 };    var cache = datajs.createDataCache(opt);    var index = 0;    function nextData() {        cache.readRange(index, 10).then(function (results) {            $('#tableData1').empty();            $("#template1").tmpl(results).appendTo("#tableData1");        });        index += 10;    }    $(document).ready(nextData);</script><input type="button" value="Next" onclick="javascript:nextData();" />. . .

「Next」ボタンを押すと、あらかじめ Prefetch されている次の 10 件 (11 件目から 20 件目のデータ) が表示され、バックエンドでは、非同期に、301 件目から 310 件目までのデータがロードされ、常に、300 件のデータが Prefetch された状態でブラウザー上に保持 (キャッシュ) されます。
Fiddler などでキャプチャーしていただくとわかりますが、この際、サービスには、下記のような $skip、$top を使用した呼び出しがおこなわれています。

http://myserver/DatajsSample/TestService.svc/Orders?$skip=20&$top=10
(21 件目から 30 件目のデータを取得する場合)

補足 : なお、上述の通り、データは (ブラウザー再表示後も) DOM Storage にキャッシュされています。(以降は、このキャッシュされたデータが使用されます。) このため、キャッシュを削除して新規にデータを取り直すには、以下の通りクリアをおこなってください。
cache.clear();

また、jQuery の dataTables プラグインなどのように、フィルター機能と組み合わせることもできるようになっています。例えば、下記のサンプル コードでは、OrderId が偶数のデータのみを抽出し、10 件ずつ表示をおこないます。([Next] ボタンを押すと、次の偶数データが 10 件 表示されます。)

. . .<script type="text/javascript">    var opt = { name: "testCache", source: '/DatajsSample/TestService.svc/Orders', pageSize: 10, prefetchSize: 300, cacheSize: -1 };    var cache = datajs.createDataCache(opt);    var index = 0;    function nextData() {        cache.filterForward(index, 10, function (item) {                return (item['OrderId'] % 2 == 0);            }).then(function (results) {            $('#tableData1').empty();            $("#template1").tmpl($.map(results, function (element) { return element.item })).appendTo("#tableData1");            index = results[9].index + 1;        });    }    $(document).ready(nextData);</script><input type="button" value="Next" onclick="javascript:nextData();" />. . .

filterForward(index, count, func) は、データの index 番目から 順方向に データを func で判定し、count 個になるまでフィルターされたデータを検索して取得します。一方、逆方向に フィルターする場合は、filterBack 関数を使用します。

補足 : 前述の readRange では、results (上記) に抽出結果のアイテムの配列が入ります。しかし、上記の filterForward では、results[i].item (i = 0, 1, …) で各アイテムを取得します。このため、上記の通り、jquery.map を使用しています。
また、filterForward メソッドの第 1 引数にアイテムの開始インデックスを指定する必要がありますが、results[i].index (i = 0, 1, …) によって、現在取得されている各アイテムのインデックスが取得できるため、上記コードの通り、これを使用して、次の開始インデックスを指定できます。

なお、これら filterForward、filterBack によるフィルター処理は、クライアント サイド (ブラウザー側) でおこなわれるので注意してください。(サーバー サイドのフィルターは、従来通り、$filter などを使用する必要があります。)

また、上記の cache オブジェクトが内部で使用するストア (store) ですが、このストアのみを単独で使用する API も提供されています。この store API については、以下に記述されていますので、参考にしてみてください。(ここでは、説明を省略します。)

[CodePlex] datajs store API :

http://datajs.codeplex.com/wikipage?title=datajs%20store%20API

 

Categories: Uncategorized

Tagged as:

1 reply»

Leave a Reply