の続き
第十五章 読み取り専用のWebサービスの設計
リソース設計
Webサービスの設計と一言で言ってもシステム全体の設計や、DBの設計、クラス設計など様々な設計がある。その中でもリソース設計は「クライアントとサーバの間のインダーフェース設計」のことを指す。本書ではリソース設計について扱っている。
リソース指向アーキテクチャのアプローチ
ソフトウェア開発には様々な設計指針がある中でリソース設計には一般的な設計手法が存在しない。唯一存在するのは「RESTful Webサービス」が推奨している「リソース指向アーキテクチャ」の設計アプローチで以下のステップからなる設計手法。
- Webサービスで提供するデータを特定する。
- データをリソースに分ける。
- (各リソースに対して)
- URIで名前をつける
- クライアントに提供するリソースの表現を設計する。
- リンクとフォームを利用してリソース同士を結びつける。
- イベントの標準的なコースを検討
- エラーを検討
これらのステップを郵便番号検索サービスの設計を例に順を追って紹介する。このサービスの要件は以下の通り。
- 日本の郵便番号情報を提供
- 郵便番号の前方一致で、郵便番号情報を検索できる。
- 住所及び住所の読みで、郵便番号情報を全文検索できる。
- データはすべて読み取り専用
Webサービスで提供するデータを特定する。
サービスで提供するデータを理解し特定する作業。日本郵便が公開している郵便番号のCSVデータを元にする。
ここから、このサービスは以下の3つのデータを持つ。
- 7桁の郵便番号
- その郵便番号が表現する住所
- 住所のカタカナ読み
データをリソースに分ける。
リソースとはWeb上に存在する名前のついた情報。このWebサービスで提供する情報は、
- まず郵便番号で、郵便番号は住所と住所の読み情報を持っている。
- 郵便番号や住所を検索する機能がある。
- このような機能をリソースに落とし込む場合、機能の結果をリソースとして捉える。
- よって、検索結果がリソースとなる。
- 地域に関する情報
- 都道府県、市町村と上位の地域が下位の地域の情報を内包する形
- ユーザーが最初にアクセスした結果(トップ画面)
リソースにURIで名前をつける。
郵便番号リソース
「1120002」「112-0002」といった郵便番号を使う。
http://zip.ricollab.jp/1120002
検索結果リソース
検索キーワードの入力を必要とし、クライアントからの入力を受け取る場合はクエリパラメータを使う。
http://zip.ricollab.jp/search?q=小石川
「小石川」は実際にはUTF-8で文字エンコードされる。
地域リソース
階層構造で地域リソースを表現
http://zip.ricollab.jp/東京都/文京区/小石川
トップリソース
ルートとなるURIを与える。
http://zip.ricollab.jp/
クライアントに提供するリソースの表現を設計する。
各リソースの表現形式を設計。代表的な表現形式として「XML表現」「軽量フォーマット表現」「マルチメディア表現」がある。今回のWebサービスだとマルチメディア表現は必要なく、XML表現と軽量フォーマット表現の利用を考える。
XML表現
この表現形式を選択するにあたって独自フォーマットを作り出さないという重要な指針がある。まずは既存のフォーマットとして有名なXHTMLやAtomの利用を検討する。今回のケースだと投稿フォームを作るわけではないのでXHTMLのほうが向いていると言える。
軽量フォーマット表現
代表的なフォーマットとしてJSON, YAML, CSVがある。
今回の場合、zip.ricollab.jp
以外のサイトで提供しているHTML中のスクリプトからこのAPIを直接呼び出すにはJSONPを利用するしかないのでJSONが適していると思われる。
URIで表現を指定
http://zip.ricollab.jp/1120002.json
http://zip.ricollab.jp/東京都/文京区/小石川.html
JSONPを用いてcallback関数名を渡す場合
http://zip.ricollab.jp/東京都/文京区/小石川.json?callback={コールバック関数名}
クエリパラメータを受け取り検索結果を返すような場合、拡張子ではなくクエリパラメータで表現を指定。
typeを省略した場合、デフォルトはXHTML
http://zip.ricollab.jp/search?q=小石川&type=html
リンクとフォームを利用してリソース同士を結びつける。
検索結果リソース
検索キーワードに合致する郵便番号と住所の一覧を表示。
例えば「112」の検索結果を表示するときは、「112」以降の住所一覧のリンクを入れる。また、情報量が多くページを分割する場合などは、次のページを示すリンクもつける。jsonだとnextというメンバを追加する。
地域リソース
地域リソースが含む下位の地域リソースや郵便番号リソースにリンク。
例えば「東京都」で検索した場合、「東京都文京区」など下位の地域リソースのリンクが示される。
郵便番号リソース
最終目的のリソースではあるが、例えば「1120002」の検索結果から「東京都」や「東京都文京区」などへのリンクを用意しておくことで別のアプリケーション状態に遷移できる。
トップレベルリソース
10万件以上のリソースへのリンクを表現するわけには行かないので、都道府県一覧などを入れるのが無難。
イベントの標準的なコースを検討
- 郵便番号を検索するコース
- 住所から郵便番号を検索するコース
- 地域リソースの階層をたどりながら郵便番号を選択するコース
エラーの検討
今回のサービスだと読み取り専用のためエラーが起こる可能性が低いが、
- 存在しないURIを指定
- クエリパラメータの指定のし忘れ
- 読み取り専用なのにPUTなどのメソッドを使用
した場合にエラーが返ると思われる。
第十六章 書き込み可能なWebサービスの設計
前章で作成した読み取り専用の郵便番号検索サービスに以下の機能を追加することを考えて、書き込み可能なWebサービスの設計について考える。追加する機能は以下の通り
- 郵便番号リソースの作成
- 郵便番号リソースの更新
- 郵便番号リソースの削除
- バッチ処理
- トランザクション
- 排他制御
リソースの作成
ファクトリリソースによる作成
ファクトリリソース自体はWebサービスによってあらかじめ作成しており、POSTで新しいリソースを作成する。
PUTによる作成
郵便番号によってリソースのURIがあらかじめ決まっているためPUTでも作成可能。URIはクライアントが知っているためLocationヘッダは不要。PUTで実装する利点は以下。
- POSTをサポートする必要がなくなるため、サーバ側の実装が楽になる。
- クライアントが作成と更新を区別しなくて良いため、クライアント側の実装が楽になる。
一方で欠点として
- クライアントがURI構造を知らなければならない。
- リクエストの見た目上は作成なのか更新なのかの区別がつかなくなる。
リソースの更新
基本的にはPUTで実施。
バルクアップデート
更新したいリソース全体をメッセージボディに入れる方法。クライアントの実装が簡単である反面、送受信するデータが大きくなるという欠点もある。
パーシャルアップデート
バルクアップデートはネットワーク帯域をたくさん使ってしまうため、Ajaxなどの非同期通信では不都合。このような場合に部分的にデータを送信する更新方法をパーシャルアップデートという。例えば住所リソースの県名だけを変える場合。
送受信するデータが少なくなる反面、GETしたリソースの一部を修正してそのままPUTするという使い方ができなくなる。
更新できないプロパティを更新しようとした場合
400 Bad Requestを返して、プロパティの更新ができないことをクライアントに伝えるのが良いが、サーバ側で自動的に値を更新している場合などは更新をリクエストしてきたとしても無視して200 OKを返すのが良い。
リソースの削除
DELETEを用いてリソースの削除をするが、このときに気をつけなければならないのがリソースの配下に子リソースが存在するとき。このとき親リソースに従属する子リソースは親リソースの削除に伴って削除するべき。
バッチ処理
大量のリソース(郵便番号)を作成したり更新したりするときに用いる。バッチ処理で更新をするときPOSTを用いるが、これはPUTだと更新対象のリソースをURIで指定しなければならないため。
バッチ処理のレスポンス
このリクエストを受け付けたサーバはJSON配列を分解し、それぞれの更新処理を行うがこの処理中に何かエラーが起きたとき、設計方針としては2つある。
- バッチ処理をトランザクション化して途中で失敗した場合は何も処理しないことにする。
- どのリソースへの処理が成功してどれが失敗したのかをクライアントに伝える。(以下の2つの方法)
- 207 Multi-StatusとWebDAVの<D:multistatus>要素を組み合わせる方法
- 複数の<D:status>要素を用いてバッチ処理一つ一つに対してステータスコードを返す。
- 200 OKと独自のフォーマットを組み合わせる方法
- statusとdescription要素を持ったJSON配列を返す。
- 207 Multi-StatusとWebDAVの<D:multistatus>要素を組み合わせる方法
トランザクション
複数のリソースにまたがった変更を人まとまりに扱うトランザクションが必要なときに、トランザクションではそれらのリソースで処理が一部失敗した場合に元の状態に戻すことを保証する。
解決すべき課題
複数リソースの削除をする場合を考える。例えば3つのリソースの削除をしようとして、2番目のリソースで500 Internal Server Errorが返る可能性がある。このような場合にまとめて削除するのか全て削除しないのかを保証するための実装を考える。
トランザクションリソース
文字通りトランザクション状態を保持するリソースのこと。リソースの状態はセッション状態ではないため、ステートレス性の原則には反しない。
HTTPの統一インターフェースでは実現できない処理に遭遇したときは、新たなリソースを導入して解決を図るのがRESTfulな設計の定石。
トランザクション処理
- ファクトリリソース(リソースを作成するための特別なリソース)にPOSTを送信
- 生成したトランザクションリソースに削除したいリソースのURIを追加。PUTでリソースを生成。
- トランザクションリソースに処理を実行する旨を伝える。(今回はPUTでコミットを伝えるデータを書き込む。)
- 200 OKが返ってきた場合は成功。実行が失敗した場合は4xxか5xxのステータスコードが返る。
- 全て成功すればトランザクションリソースを削除する。
トランザクションリソース以外の解決方法
- バッチ処理
今回の例だと複数リソースの削除リクエストをPOSTで一括送信し、サーバ内の実装で全て削除するか1つも削除しないことを保証する。
- 上位リソースに対する操作
親要素を削除したときにその子要素を必ず全て削除するか、1つも削除しないかをサーバ側で実装する。
排他制御
複数のクライアントが同時に1つのリソースを編集して競合が起きないように1つのクライアントのみが編集するように制御する処理。排他制御には悲観的ロックと楽観的ロックの2つの方法がある。
悲観的ロック
実現方法は2つあり、
- WebDAVのLOCK/UNLOCKメソッドを使う方法
- リクエストにLOCKを用いる。
- ロックの種類としてTimeoutヘッダや、Authorizationヘッダ、<D:lockscope>、<D:locktype>などがある。
- ロックが成功すると200 OKと<D:prop>要素が返る。
- <D:activelock>や<D:depth>、<D:locktoken>要素などがある。
- (ロック済みのリソースを編集するにはIfヘッダでロックトークンを指定しなければならない。指定しないと423 Lockedが返る。)
- ロックトークンを正しく指定してPUTを送信(リソースを更新)
- 更新が完了したらUNLOCKを使用してロックを解除、ロックトークンをLock-Tokenヘッダで指定。
- リクエストにLOCKを用いる。
- 独自のロックリソースを使う方法
上の方法はWebDAVのデータモデルに最適化した手法であり、それ以外のリソースに適用しようとすると少しオーバースペックなため独自のロックリソースを使う方法もある。ここではロックを表現する子リソースを新しく導入することでロックの実現を考える。- ロックリソースを作成
- ロックしたいリソースのURIに対してPOSTでロック情報を送信
- ロックリソースが存在している間は対象のリソースはロックしていることになる。
- ロック中のリソースに誰かがロックをかけようとすると423 Lockedか400 Bad Requestが返る。
- ロックの解除はタイムアウトかロックリソースの削除で実現
楽観的ロック
悲観的ロックはシステムのスケールが大きくなるほど問題が発生する。世界中の不特定多数のユーザが同時に編集するWikiのようなシステムの場合、文書をロックしたままずっと編集できないなどの問題が生じる。
楽観的ロックは常に同じ文書を複数人が編集し続けることはあまりないという経験則から、通常の文書では文書をロックせずに競合が起きたときに対処するしくみ。実装方法には条件付きPUTと条件付きDELETEを用いる。
- 条件付きPUT
- ETagの値を得るためにリソースをGETで取得。
- If-Matchを用いてリソースに変更がないことをETagで確認してリソースを更新
- 条件付きDELETE
- 条件付きPUTと同様
- 他人が修正したのを知らずにリソースを削除するミスを防止できる。
- 412 Precondition Failed
- リソースが変更されていた場合に返る。
- 対処法
- 競合を起こしたユーザに確認した上で更新または削除
- 競合を起こしたデータを競合リソースとして別リソースに保存
- 競合を起こしたユーザに変更点を伝え、マージを促す
設計の指針
- なるべくシンプルに保つ
- 困ったらリソースに戻って考える
- ほんとに必要ならPOSTでも何でもできる
第十七章 リソースの設計
リソースの設計は未だに定石がないため何から手を付けて良いのかわからなくなるが、本章で既存の設計手法で得られた成果物を元にリソースを設計する手法を紹介。リソースの設計手法として
- 関係モデルのER図
- オブジェクト指向モデルのクラス図
- 情報アーキテクチャ
を紹介する。
関係モデルのER図
以下のようにリソースを設計する。
- 関係モデルのER図を記述
- 中心となるテーブルからのリソースの導出
- リソースが持つデータの特定
- 検索結果リソースの導出
階層の検討
関係モデルを見ただけでは階層構造がわかりにくいので関係モデルの隠れた階層構造は別途ドキュメントなどから理解し、必要であればリソースの設計に反映する。
リンクによる結合
リソース間を結合するリンクの設計にはER図の関連が利用できる。
オブジェクト指向モデルのクラス図
- 郵便番号データのクラス図
- 主要データクラスからのリソースの導出
- オブジェクトの操作結果リソース
階層の検討
クラス図には下位クラス、上位クラスがありそれらはhas-aの関係にある。このようなクラスの包含関係は階層構造に利用できる。
リンクによる結合
リソースそのものを表現するオブジェクトは相互に参照を持つ。この参照はそのままリンクとして利用できる。
情報アーキテクチャ
Webデザインのような複雑なデザインを情報分類の観点から整理して、受け手にとって情報を探しやすくしたり、分かりやすく伝えたりするための技術。
日本郵便の郵便番号検索サイトを元に情報アーキテクチャを考える。
このサイトには以下のページがある。
- トップページ
- 全国地図からの検索
- 住所での検索
- 郵便番号での検索
- が用意されている。
- 全国地図からの検索
- 都道府県から市区群、そこから市区町村のページ
- 住所での検索
- 検索キーワードにマッチした地域の一覧を表示、町域へとリンク
- 郵便番号での検索
- パンくずリスト
リソース設計で最も重要なこと
- WebサービスとWeb APIを分けて考えない。