December 2015
Volume 30 Number 13
データ ポイント - Aurelia と DocumentDB: 連携までの道のり (第 2 部)
ここ数回取り上げている 2 つのテクノロジは、新しい JavaScript フレームワークの Aurelia と、NoSQL ドキュメント データベース サービスの Azure DocumentDB です。6 月のコラムでは、DocumentDB (msdn.com/magazine/mt147238) の概要を説明しました。また、9 月のコラムでは、Aurelia でデータ バインド (msdn.com/magazine/mt422580) を実行しました。この 2 つのテクノロジに対する興味は依然として尽きることはなく、アプリケーションのフロント エンドに Aurelia を、データ ストアに DocumentDB を使用して、この 2 つのテクノロジを連携させることを考えました。好奇心がもたらす恩恵 (忍耐力) があったとはいえ、この 2 つの新しいテクノロジにも JavaScript フレームワーク全般にも経験が足りなかったため、この連携の試みは何度も行き詰まることになります。この連携を実現したという情報はなかった (少なくとも公開されていなかった) ため、さらに苦境に立たされました。どのような試みが失敗したのかは、2 部構成の連載の初回、11 月のコラム (msdn.com/magazine/mt620011) で取り上げました。明白かつ簡単な方法は、最初の Aurelia コラムで取り組んだ Web API の代わりに、DocumentDB との対話をラップする既存の Web API を使用することですが、これは採用しませんでした。挑戦のしがいがなく、面白みがあまりないと考えたためです。
最終的には、Node.js という別のサーバー側ソリューションを使用することにしました。DocumentDB Node.js SDK と Aurelia ではない別のフロント エンドを使用する既存の例も見つかりました。11 月のコラム後半では、この例を慎重に調査し、学んだ教訓を紹介しました。そこで今度は Aurelia フロント エンドと DocumentDB の中継点に Node.js を使用して、9 月のコラムに掲載した Ninja の例を作り直すことにしました。データ関連の問題に限らず、つまずきやすい問題はたくさんありました。Aurelia チームの中心メンバーから数多くの支援を受けました。特に Patrick Walters (github.com/pwkad、英語) は、Aurelia の機能を活用できるようにサポートしてくれただけでなく、間接的に Git スキルの研鑽を促してくれました。今回は、たどり着いたソリューションの中からデータのアクセス、更新、バインドに関係する部分に重点を置いて説明します。付属コード サンプルの ReadMe ファイルに、独自の DocumentDB をセットアップして、このソリューション用にデータをインポートする方法を記載しています。また、サンプルを実行するための要件をセットアップする方法も説明しています。
ソリューションのアーキテクチャ
まず、ソリューションの全体アーキテクチャを示します (図 1 参照)。
図 1 ソリューションの全体構造
Aurelia はクライアント側フレームワークなので、ビューとビュー モデルのペアに基づく HTML のレンダリング、ルーティング、データ バインドやその関連タスクなど、クライアント側の要件のみを処理します (クライアント側のコードはすべて、Internet Explorer、Chrome などのブラウザーで利用可能な開発者ツールを使用してデバッグできます)。このソリューションのクライアント側コードはすべて public/app/src フォルダーにあり、サーバー側のコードはルート フォルダーにあります。Web サイトの実行時、クライアント側のファイルはブラウザーの互換性を確保する目的で Aurelia によってまとめられ、"dist" というフォルダーに配布されます。これらのファイルがクライアントに送信されます。
(私のように) Microsoft .NET Framework で Web 開発を行ったことがあれば、サーバー側のコードは見慣れたものです。.NET 環境では、WebForms の分離コード、コントローラー、およびその他のロジックは通常、サーバーに配置する DLL にコンパイルします。もちろん、これは ASP.NET 5 で大きく変わります (今回のプロジェクトに取り組んだおかげで、ASP.NET 5 に対応する準備も整いました)。今回の Aurelia プロジェクトにはサーバー側のコードも含めていますが、これは単なる Node.js という JavaScript コードです。このコードは DLL にコンパイルされず、独自の個別ファイルとして残ります。もっと重要なのは、このコードがクライアントに送り込まれず、公開されないことです (もちろん、いつものようにセキュリティ対策によって大きく左右されます)。前回説明したように、このコードをサーバーにとどめる理由は、DocumentDB と対話するための資格情報を格納する方法が必要だからです。Node.js を使う経路を試すことが目的なので、前述の SDK により、DocumentDB との対話を簡単にしています。この SDK の使用方法の基本について参考になる DocumentDB Node 例があったことは、非常に助けになりました。
今回のサーバー側コード (今回の API) は、以下に示す 4 つの重要な要素で構成されます。
- 今回の API の関数を簡単に見つけられるように、ルーターとして機能する api.js ファイル
- getNinjas、getNinja、updateDetails などの API 関数を含む、ninjas.js コア モジュール
- DocumentDb との対話を実行する (上記の API 関数によって呼び出される) DocDbDao コントローラー クラス
- 関連データベースとコレクションを必ず存在させる方法を把握する DocumentDb ユーティリティ ファイル
クライアント、サーバー、およびクラウドの接続
このコラムでこれまでに紹介した Aurelia サンプルでは、すべての ninjas を取得するメソッドは、.NET や Entity Framework を使用して SQL Server データベースと対話する ASP.NET Web API に対して、次のように HTTP 呼び出しを直接行っていました。
retrieveNinjas() {
return this.http.createRequest
("/ninjas/?page=" + this.currentPage + "&pageSize=100&query=" +
this.searchEntry)
.asGet().send().then(response => {
this.ninjas = response.content;
});
}
この呼び出しの結果を、クライアント側のビューとビュー モデル (ninjaList.js と ninjaList.html) のペアに渡してページを出力します (図 2 参照)。
図 2 Aurelia Web サイトの Ninja リスト
新しいソリューションでは、このメソッドの名前を getNinjas に変更して、サーバー側の Node.js API を呼び出すようにします。今回は、httpClient ではなく、もっと高度な httpClient.fetch (bit.ly/1M8EmnY、英語) を使用して、次のように呼び出しを行います。
getNinjas(params){
return this.httpClient.fetch(`/ninjas?q=${params}`)
.then(response => response.json())
.then(ninjas => this.ninjas = ninjas);
}
httpClient は、ベース URL を把握する別の場所で構成しています。
上記の fetch メソッドは、"ninjas" という語句を含む URI を呼び出しています。しかし、この URI はサーバー側 API の ninjas.js を参照していません。これは /foo (サーバー側のルーターで解決されるランダムな参照) と考えられます。今回のサーバー側ルーターには Express を使用します。これは Aurelia がクライアント側のみに対処するためです。このルーターは、api/ninjas に対する呼び出しを、ninjas モジュールの getNinjas 関数へルーティングするように指定しています。このルーティングを定義する api.js のコードを以下に示します。
router.get('/api/ninjas', function (request, response) {
ninjas.getNinjas(req, res);
});
この getNinjas 関数 (図 3) では、文字列内挿機能 (https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/template_strings) を使用して、DocumentDB をクエリする SQL を表す文字列を構築後、そのクエリを実行して結果を返すようにコントローラーに依頼します。UI で name プロパティにフィルターが要求されている場合は、このクエリに WHERE 句を追加します。メインのプロジェクション クエリでは、ページに必要な関連プロパティ (ID など) のみを指定します (DocumentDB のプロジェクション クエリの詳細については、https://azure.microsoft.com/ja-jp/services/documentdb/ を参照)。構築したクエリを、docDbDao コントローラーの find メソッドに渡します。find メソッドは、連載初回の find メソッドと同じもので、config.js に格納されている資格情報を指定して Node.js SDK を使用し、ninjas をデータベースにクエリします。getNinjas は、次にこのクエリの結果を取得して、要求元のクライアントに返します。DocumentDB への呼び出しは JSON 形式で結果を返しますが、それでも response.json 関数を明示的に使用して結果を返す必要があります。これにより、結果が JSON 形式であることを呼び出し元に知らせます。
図 3 ninjas.js サーバー側モジュールの getNinjas 関数
getNinjas: function (request, response) {
var self = this;
var q = '';
if (request.query.q != "undefined" && req.query.q > "") {
q= `WHERE CONTAINS(ninja.Name,'${req.query.q}')`;
}
var querySpec = {
query:
`SELECT ninja.id, ninja.Name,ninja.ServedInOniwaban,
ninja.DateOfBirth FROM ninja ${q}`
};
self.docDbDao.find(querySpec, function (err, items) {
if (err) {
// TODO: err handling
} else {
response.json(items);
}
})
},
編集要求へのクライアント側の応答
図 2 でペンのアイコンをクリックすると、ninja を編集できます。以下は、このページのマークアップで作成しているリンクです。
<a href="#/ninjas/${ninja.id}" class=
"btn btn-default btn-sm">
<span class="glyphicon glyphicon-pencil" />
</a>
先頭行のペンのアイコンをクリックします。今回の場合はこの行の ID が "1" なので、以下の URL になります。
http://localhost:9000/app/#/ninjas/1
app.js にあるクライアント側の Aurelia ルーティング機能を使用することで、このような URL パターンが要求された場合は、edit モジュールを呼び出して、edit ビュー モデルの activate メソッドに、ワイルドカード (*Id) によって示す ID をパラメーターとして渡します。
{ route: 'ninjas/*Id', moduleId: 'edit', title:'Edit Ninja' },
Edit はクライアント側の edit.js モジュールを表します。このモジュールが、edit.html ビューとペアになっています。edit モジュールの activate 関数は、同じモジュールの retrieveNinja 関数を呼び出して、要求された ID を渡します。
retrieveNinja(id) {
return this.httpClient.fetch(`/ninjas/${id}`)
.then(response => response.json())
.then(ninja => this.ninja = ninja);
}
編集要求のサーバー側 API への受け渡し
httpClient.fetch を使用して、API の api/ninjas/[id] (今回の例では api/ninjas/1) を要求します。サーバー側のルーターは、このパターンの要求を受け取った場合、ninjas モジュールの getNinja 関数にルーティングするよう指示します。ルーティング内容は以下のようになります。
router.get('/api/ninjas/:id', function(request, response) {
ninjas.getNinja(request, response);
});
getNinja メソッドは docDbDao コントローラーに別の要求を行います。今回は getItem 関数に要求を行い、DocumentDb から ninja データを取得します。結果は、DocumentDB データベースに格納されている JSON ドキュメントです (図 4 参照)。
図 4 DocumentDb Ninjas コレクションに格納されている、今回要求したドキュメント
{
"id": "1",
"Name": "Kacy Catanzaro",
"ServedInOniwaban": false,
"Clan": "American Ninja Warriors",
"Equipment": [
{
"EquipmentName": "Muscles",
"EquipmentType": "Tool"
},
{
"EquipmentName": "Spunk",
"EquipmentType": "Tool"
}
],
"DateOfBirth": "1/14/1990",
"DateCreated": "2015-08-10T20:35:09.7600000",
"DateModified": 1444152912328
}
クライアントへの結果の返却
上記の JSON オブジェクトが ninjas.getNinja 関数に返され、続いて呼び出し元 (今回はクライアント側の edit.js モジュール) に返されます。Aurelia では、edit.js を edit.html テンプレートにバインドして、この内容を表示するために設計したページを出力します。
edit ビューにより、ユーザーは、忍者 (ninja) の名前 (文字列)、誕生日 (文字列)、一族 (ドロップダウン)、および御庭番としての任務に就いているかどうかという、4 つのデータを変更できます。一族のドロップダウンでは、カスタム要素という特別な種類の Web コンポーネントを使用します。カスタム要素の Aurelia 実装は、独特の実装です。今回はデータをバインドするためにこの要素を使用し、この要素がデータ列になっているため、ここからはこの要素のバインド方法について示します。
Aurelia のカスタム要素によるデータ バインド
Aurelia の他のビューとビュー モデルのペアと同様、カスタム要素は、ビューのマークアップとビュー モデルのロジックで構成されます。図 5 に、一族のリストを提供する 3 つ目の関連ファイル clans.js を示します。
図 5 Clans.js
export function getClans(){
var clans = [], propertyName;
for(propertyName in clansObject) {
if (clansObject.hasOwnProperty(propertyName)) {
clans.push({ code: clansObject[propertyName], name: propertyName });
}
}
return clans;
}
var clansObject = {
"American Ninja Warriors": "anj",
"Vermont Clan": "vc",
"Turtles": "t"
};
この要素のビュー (dropdown.html) では、以下のように Bootstrap の "select" 要素を使用します。私はいまだにこの要素をドロップダウンだと思っています。
<template>
<select value.bind="selectedClan">
<option repeat.for="clan of clans" model.bind="clan.name">${clan.name}</option>
</select>
</template>
上記で注目するのは value.bind と repeat.for です。Aurelia によるデータ バインドとしてこれまでのコラムに登場しているので、既におなじみだと思います。select 要素内の option は、clans.js で定義されている clan モデルにバインドされ、clan の名前を表示します。今回の Clans オブジェクトは名前とコード (ここでは使用しません) で構成されるシンプルなオブジェクトなので、単純に value.bind を使用してもかまいませんが、覚えやすい優れたパターンなので、あえて model.bind を使用しています。
dropdown.js モジュールは、このマークアップと clans とを接続します (図 6 参照)。
図 6 Dropdown.js 内のモデルのカスタム要素コード
import $ from 'jquery';
import {getClans} from '../clans';
import {bindable} from 'aurelia-framework';
export class Dropdown {
@bindable selectedClan;
attached() {
$(this.dropdown).dropdown();
}
constructor() {
this.clans = getClans();
}
}
さらに、カスタム要素を使えば、以下のように edit ビューでもっと簡単なマークアップを使用することもできます。
<dropdown selected-clan.two-way="ninja.Clan"></dropdown>
このマークアップでは、Aurelia 固有の機能を 2 つ使用しています。まず、今回のプロパティは、ビュー モデルでは selectedClan、マークアップでは selected-clan になっています。Aurelia の表記規則では、HTML が属性を小文字にするよう求めることから、すべてのカスタム プロパティのエクスポート名を小文字にし、ハイフンでつなぎます。つまり、ハイフンでつながれている部分は大文字が使われていたと想定します。次に、value.bind ではなく、今回は two-way (双方向) バインドを明示的に使用して、選択項目が変化したら clan から ninja に再バインドされるようにしています。
もっと重要なのは、今回のカスタム要素を別のビューで再利用するのが非常に簡単になることです。今回は読みやすくなり、メンテナンスが容易になるだけですが、大きなアプリケーションでは、マークアップとロジックを再利用できることが大きなメリットになります。
DocumentDB への編集結果の反映
今回のマークアップは、データベースのから 2 つの装備を読み取って表示しています。説明を簡単にするために、今回は装備データの編集には触れません。
今回作業する最後の部分では、クライアントでの変更が API に返された後、それを DocumentDB に送信します。この操作は、[Save] ボタンによってトリガーします。
[Save] ボタンのマークアップでは、click.delegate という別の Aurelia パラダイムを使用します。click.delegate は JavaScript のイベント デリゲーションを使用して、edit.js で定義されている save 関数に操作をデリゲートできます。
この save 関数は、getNinja で定義した ninja プロパティの関連プロパティを使用して、新しいオブジェクト ninjaRoot を作成します。次にこのオブジェクトをマークアップにバインドして、ユーザーがブラウザーからこのオブジェクトを更新できるようにします (図 7 参照)。
図 7 ninjas.save 関数
save() {
this.ninjaRoot = {
Id: this.ninja.id,
ServedInOniwaban: this.ninja.ServedInOniwaban,
Clan: this.ninja.Clan,
Name: this.ninja.Name,
DateOfBirth: this.ninja.DateOfBirth
};
return this.httpClient.fetch('/updateDetails', {
method: 'post',
body: json(this.ninjaRoot)
}).then(response => {this.router.navigate('ninjaList');
});
}
次に save 関数は、httpClient.fetch を使用して updateDetails という API URL を要求し、要求本文として ninjaRoot オブジェクトを渡します。ここでは、get メソッドではなく、post メソッドを指定しています。API ルーターは Node.js に対して、ninjas モジュールの updateDetails メソッドにルーティングするよう指示します。
router.post('/api/updateDetails', function(request,response){
ninjas.updateDetails(request,response);
});
ここで、ninjas.js のサーバー側 updateDetails を見ておきます。
updateDetails: function (request,response) {
var self = this;
var ninja = request.body;
self.docDbDao.updateItem(ninja, function (err) {
if (err) {
throw (err);
} else {
response.send(200);
}
})
},
request body に格納されている ninjaRoot を抽出して ninja 変数に設定後、この変数をコントローラーの updateItem メソッドに渡します。第 1 部のコラムとは違って、updateItem に少し変更を加え、ninja 型を格納できるようにしています (図 8 参照)。
図 8 Node.js SDK を使用して DocumentDB と対話する、docDbDao コントローラーの updateItem メソッド
updateItem: function (item, callback) {
var self = this;
self.getItem(item.Id, function (err, doc) {
if (err) {
callback(err);
} else {
doc.Clan=item.Clan;
doc.Name=item.Name;
doc.ServedInOniwaban=item.ServedInOniwaban;
doc.DateOfBirth=item.DateOfBirth;
doc.DateModified=Date.now();
self.client.replaceDocument(doc._self, doc, function (err, replaced) {
if (err) {
callback(err);
} else {
callback(null, replaced);
}
});
}
});
},
UpdateItem は、id を使用してデータベースからドキュメントを取得し、そのドキュメントの関連プロパティを更新してから、SDK DocumentDBClient.replaceDocument メソッドを使用して Azure DocumentDB データベースに変更をプッシュします。callback は、処理の完了を通知します。この callback はupdateDetails メソッドによって呼び出され、API を呼び出したクライアント モジュールに応答コード 200 を返します。クライアント側の save メソッドに戻ると、callback が ninjaList にルーティングされているのがわかります。そのため、更新が正しくポスト (post) されると、ユーザーにオリジナルの ninjas リスト ページが表示されます。ninja に行った編集が、このリストに反映されます。
納得できましたか ?
今回のソリューションでは、困難や行き詰まりを何度も体験しました。Aurelia と DocumentDB データベースとの対話を実現することが主な目標でしたが、このようなテクノロジのメリットを生かすことも考えていました。そのためには、JavaScript を取り巻く多くのことを理解し、Node のインストールを管理し、Node.js をデバッグできることから Visual Studio Code を初めて使用し、DocumentDB について多くを学ばなければなりませんでした。今回紹介した単純なサンプルでも、多くのことを実行してみなければなりません。このコラムをお読みになれば、あるテクノロジと別のテクノロジを連携させる基本をご理解いただけると思います。
注意すべき重要な点は、NoSQL データベースなどの DocumentDB は、大量のデータを扱うことを目的に設計されていることです。今回の例で使用したような、ごく少量データではコスト効率がよくありません。しかし、データベースとの接続やデータ操作の機能を調べることが目的ならば、大量のデータは必要なく、5 つのオブジェクトでも十分です。
Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、彼女は O'Reilly Media から出版されている『Programming Entity Framework』(2010 年) および『Code First』版 (2011 年)、『DbContext』版 (2012 年) を執筆しています。彼女の Twitter (@julielerman、英語) をフォローして、juliel.me/PS-Videos (英語) で彼女の Pluralsight コースをご覧ください。
この記事のレビューに協力してくれた技術スタッフの Patrick Walters に心より感謝いたします。
Patrick Walters は、Aurelia 開発者コミュニティの一員で、公式チャネルの Aurelia gitter で質問に答えるのを楽しんでいます。