mozaic.fm #7 RESTや#mozaicfm REST を聴いての感想、それから「Web+DB vol82のWebAPIデザインの鉄則」に触発されたので書こうと思う。
REST設計について
WebAPIを設計するうえでRESTが重要であることは周知のとおりである。
“Constraints are liberating”「制約は自由をもたらす」
@t_wadaさんがおっしゃっているように、RESTを前提にすれば、「アーキテクチャとしてもそうだし、アプリケーションフレームワークも「適切な制約」を設けることで設計のコストが下がる」という大きなメリットが生まれる。
しかし、相変わらずリソース設計やらインターフェース設計やらで悩んでおられる方も多いと聞く。 その一方で個人的には適切なフレームワークを使えばREST設計で悩まなくてもよいはず(※3)という思いもある。
インターフェース設計などの基本設計をフレームワーク側で用意してあげれば、普通に設計していくだけで自然とRESTfulになっていくような開発が可能になる。これはReflexWorksが目指しているところでもある。
そういう意味で、ちゃんとしたフレームワークさえあれば、今は開発者がRESTの基本をしっかり学んですべて設計する時代じゃないかもしれないと個人的には思っている。
URIの制約をどうするか
ではどのような制約を設ければ自然とRESTfulに設計できるのか。
まず私が一番大事だと思っているのはURIの制約である。
URIはリソースを指し示す重要な表現であるが、これは単にリソースの場所を示すものであって、表現形式や検索条件など他の情報を含めるべきではないと考えている。(ただし、クエリパラメータは別)
例えば、以下のURIはよくあるものだが、このなかにはバージョン番号や「API」で「datastore」であるということ、また階層によってコレクション、またはエントリであることを説明する情報がURIに含まれてしまっている。
/v1/api/datastore/{コレクションID}/{オブジェクトID}
私はこれをURIの「お仕着せ」と呼んでいる。
一方、単にリソースの場所を示すURIは以下のようになる。
/d/{リソースID1}/・・/{リソースIDn}
これにはバージョンやAPIなどを示す情報はなくコレクションやエントリの区別もない。
階層が/によって多段に表現されているが単にリソースがあることを示すだけである。
例えば、{リソースID1}/{リソースID2} は、リソースID1の配下にID2があることを示しているが、ちょうどファイルシステムのフォルダをイメージするとわかりやすいかもしれない。
/d/app/index.html とすれば、/appフォルダの下にindex.htmlが入っているというように直感的にわかる。これを、リソースにおいても/d/invoice/master/・・ などと階層で表現したいと考えている。
ちなみに、/v1/api/datastoreも階層表現されているように見えるがこの階層には意味はない。その証拠に、/v1_api_datastoreとしても同じことである。またURIの設計上だけで実際にエントリがあるわけでもない。これはURLが単に固定されているだけの話であって制約というより不自由なだけである。
繰り返しになるが、URIにリソースのアドレス以外の特別の意味を持たせてはいけない。
そういう固定的でお仕着せ的な設計がAPIの齟齬を生む。そして最終的には管理できなくなりバージョニングが必要という話になってしまう。
URIが単にリソースを示すだけであれば、APIの仕様を見ながらコーディングする必要もなく、バージョニングも必要ない。(どうしても必要なら?v1とかクエリーパラメータで区別すべきというのが私の意見。<関連> You Need Not API verson2)
ただ、先日ある開発者に、ReflexWorksにはGoogle Drive APIのような一覧ってないのですか?って聞かれたときには、世の中はそういうものなのかなあとは思った。(その開発者にとってみれば制約よりも仕様の方がわかりやすいということだった。(※1)
オブジェクトの中の項目をURIに含めるべきか
JSONオブジェクトの構造中の特定の位置を指し示すJSON Pointerというのがある。
例えば、以下のオブジェクトが/d/masterに入っていたとする。
{
"customer" :[
{ "name":"foo" ,"id": "1"},
{ "name":"bar" ,"id": "2"},
・・・
],・・・
}
"customer" :[
{ "name":"foo" ,"id": "1"},
{ "name":"bar" ,"id": "2"},
・・・
],・・・
}
JSONPointerでアドレスを/customer/0/idとすれば、customer配列の0番目のidという意味になる。これを、URI(/d/master)にアドレスを直結して、/d/master/customer/0/id と表現するのはあまりうまくない。
なぜなら、先ほどのリソースの定義では、customerの下に0というリソースがあり、その下にidというリソースがあることになってしまうからだ。
HTMLの場合は同一文書内のフラグメントには#を使うが、リソースも同様であり、あくまでmasterの中なので、私なら/d/master#customer[0].id と表現する。この方がJSONっぽくて自然だと思う。
では、リソースの階層表現においてコレクションやエントリをどのように区別すればいいだろうか。
HTMLの場合は同一文書内のフラグメントには#を使うが、リソースも同様であり、あくまでmasterの中なので、私なら/d/master#customer[0].id と表現する。この方がJSONっぽくて自然だと思う。
クエリパラメータによる表現の区別
では、リソースの階層表現においてコレクションやエントリをどのように区別すればいいだろうか。
私はクエリパラメータを使って区別すればいいと考えている。
実際にReflexWorksでは、?f(feed)でコレクション、?e(entry)でエントリを意味する。
また、リソースというのはデータの集合であり、その表現はJSONやXMLやその他どの様なフォーマットであっても同様に扱えるのが理想である。ReflexWorksでは、デフォルトはJSONだが、?xでxml、?mでMessagePackのフォーマットで表現できる。
もっといえば、HTMLコンテンツであっても画像であっても同じリソースと考えることができるわけで、GET /d/app/index.html でも、 GET /d/masterでも区別する必要はない。
フレームワーク実装の難易度は上がるかもしれないが、こうすることで全てのリソースに対してRESTで統一できる。
クエリパラメータを使っての条件絞り込みは直感的でよい実装だと思う。ReflexWorksでも、?の後ろに絞り込み条件を複数指定することができる。そのときのパラメータは以下のようにエントリの中の項目(subInfo.favorite.foodやpriceなど)となる。-eq-(=)や-lt-(<)は条件式である。
リソースを直列分割して扱う「?fields」というパラメータをよく見かけるが、個人的には好きにはなれない。これは、指定されたフィールドのみを対象としたリソースを得るために使われるものだが、リソースの最小単位=エントリの括りがなくなってしまい曖昧になってしまう。例) http://xxx.xxx/{URI}?f&subInfo.favorite.food-eq-egg&price-lt-5000&l=20
上記の絞り込みの条件で得る結果はフィード(コレクション)もしくは、エントリであるべきだ。
リンクがない問題と語彙の導入
XML全盛の頃はATOM FeedのセマンティクスとRESTによってルールはうまく整理されていたように思う。(結局流行らなかったが)
ATOMの語彙は以下のようなシンプルなものであった。
title
id
link
title
rel
href
content
・・
id
link
title
rel
href
content
・・
しかし、JSONによってXMLが駆逐されてしまった。それに伴い、SOAPもWSDLもATOMも無くなった。JSONがもたらした不毛の荒野にはXMLやATOMが培ってきた財産(スキーマやリンク)は跡形無く見当たらない。
リンクのないデータは扱いにくい。PrimaryKeyのないデータと同じようなものだ。
JSONにはメディアタイプとLINKヘッダという2つの解決策があるらしいが、エントリに入らないと扱いにくいし、特にコレクションをどう表現するかが課題だと思う。
JSON Schemaでは以下のようにidやlinksという項目が導入されている。
title
id
type
links
title
rel
href
mehod
mediaType
targetSchema
id
type
links
title
rel
href
mehod
mediaType
targetSchema
リンクについていえば、AtomよりJSON Schemaの方がむしろ語彙数が多くなっているのは皮肉である。Atomに比べてJSON Schemaはむしろ冗長になっている。
リソースは自由に変換されるべきということは述べたが、私はリソース定義にはmethodやmediaType、targetSchemaという項目は必要ないと思う。
結局、ATOMの語彙でよかったのではないか。ATOMのJSON表現で。
JSONでスキーマを書くべきか
JSONもちゃんと設計したいということで構造を含むスキーマ設計がやりたくなるのはわかる。それに、スキーマレスだから設計しないでもOK(ヒャッハー)というわけにはいかない。特に業務システムであれば。
きちんと設計したうえで必要なときにいつでもスキーマを変更できるような柔軟なソフトスキーマが望まれていると思う。
ただAtomやXMLを削ってJSONを拡充するという発想は何か間違っている気がする。
今はJSON全盛とはいえ、JSON SchemaはWSDLと同じ匂いがする。ATOMに比べても語彙数が増えている。JSONに語彙を追加するのはHTML5で語彙を追加するのとはわけが違うと思うのだが、それでもなお、似たようなものを導入するつもりなのだろうか。
たしかに、JSONはXMLよりシンプルで、基本的な値などをやりとりするだけであれば、XMLよりもJSONの方がずっと簡単だし、JavaScriptを使っているのであればJSON形式は自然な形式である。
しかしスキーマは別の話だ。何でスキーマをJSONで定義するの?という素朴な疑問は湧いてくる。
構造を定義するのにお世辞にも読みやすいとはいえないJSONを使う意味は本当にあるのだろうか。
XMLはHTMLに似て宣言的なのだしマークアップに適している。スキーマ定義も宣言的なのだからXML使う方がまだましではないか。
リソース定義のためのシンプルなスキーマ言語の必要性
そして、そもそも考える順序が逆ではないかということに気づく。
先に述べたように、JSONはリソースの一つの表現にすぎないわけだから、リソースはJSON前提で考える必要はなく、逆にJSONに縛られて考えてもいけない。
JSONを拡充する方向で上がっていってもだめで、つまりは、「JSONやXMLなどの表現にとらわれない汎用的な」リソース構造を定義するスキーマ言語があればよいという考えに至る。(※2)
例えば、以下のように、項目名や型、バリデーションルールなどを1項目につき1行で記述できれば便利である。ルールは、()に型、省略でString、{}に値の範囲、=の右辺に正規表現、!で必須項目などとする。
master
customer
name=^.{0,50}$
id(int){0~999999}!
・・・
customer
name=^.{0,50}$
id(int){0~999999}!
・・・
JSONは可読性においてXMLに及ばず、利便性においてEXCELに及ばない。
JSON Schemaで管理するのはいいが結局はEXCELを捨てきれないだろう。
プロジェクトの共有文書が相変わらずEXCELのままであれば、JSONSchemaで管理すると言った人が2重メンテするハメになるのは目に見えている。
ReflexWorksでも上記のようなシンプルなスキーマ定義を採用している。そして、スキーマからエンティティオブジェクトを自動生成している。
テンプレートに項目名を記述することで自由にスキーマを定義でき、運用中に項目の追加変更が可能である。ソフトスキーマであり更新すると直ぐにシステムに反映する。
また、型の指定、親子関係や繰り返しといった構造の定義、必須チェックや最大/最小チェック、バリデーション、Index、暗号化などを指定できる。
(詳しくは、仕様の「テンプレートによるスキーマ定義」を参照してください)
(追記)
(※1) このように感じる方は意外と周りに多い。特にサービス(ビジネスロジック)を含めて考えると可読性や振る舞いの予想という意味でURI固定の方がわかりやすいという。ここではあくまでサービスを含めない静的なリソースのことを主に想定している。(ReflexWorksの/dは静的なdataの意味で、サービスは/pのプロバイダというように使い分けている。)
(※2) JSONとXMLは等価ではないし互いに可換でもないので、1つのスキーマで両対応は無理。サブセットしかサポートできないというのはおっしゃるとおり。https://twitter.com/makotokuwata/status/504954681085263872 ただ、構造や型、バリデーションなどを汎用的に定義することは可能だし、ReflexWorksで実際にやっていることでもある。
(※3) 悩まなくなったらものづくりの仕事つまんなくない?との意見をいただいた。
@yohei それにURIを設計する方は楽しくてもその残念なURIに付き合う方はたまったものじゃない。BaaSがイマイチ流行んないのは、残念なURIに付き合うコードを書きたくないからだと思う。技術的負債になるとわかっているから皆近づかないのでは。
— takezaki (@stakezaki) 2014, 8月 28