APIのバージョニングは限局分岐でやるのが良い
ちょっと前にTwitterでAPIのバージョニングをどうやるかみたいな話をしていたのですが、そのへんもやもやしているので少し整理しておきたいなと。
APIのURLを/api/v1/*とかってやるの、やめたほうがいいとおもうんだけどなぁ。いざv2を作るとなったときに、大量のコピペが発生して後悔するよ、って伝えたい。
— Kenn Ejima (@kenn) February 28, 2014
さて、これについて色々と異論・反論も含めた意見が出たのですが、まずは、大昔にURL方式︵=コントローラ分割︶でやってきて後悔したぼくが、︵5年ぐらい前から︶現在はどうやってAPIのバージョンを管理しているか?について紹介します。
基本原理としては、コピペが多発する根っこで分岐︵=コントローラ分割︶じゃなくて、必要最小限のところで限局的に分岐するのがいい、という考え方に基づきます。
一言でいうと、﹁パラメータとしてAPI versionを渡し、それをリクエスト単位でスレッドローカル変数に保存し、必要に応じて分岐する﹂というやり方です。
API versionの渡し方
具体的なAPI versionの渡し方としては、おおまかに二種類あります。 まずは、ログインしてセッションを生成する段階でパラメータとして渡す方法。ものすごく簡素化していうと/api/session/create?id=foo&password=bar&api_version=1みたいな感じで、認証に成功したら短命なsession_tokenを受け取り、以降はサーバ側で保存されたAPI versionが適用されるので、クライアント側からはapi_versionのような付帯情報を毎回送る必要がなく、session_tokenだけをキーとして送ればよくなります。 このやり方は、グループチャットのLingrや対戦ゲームインフラのPankiaのような、オンライン・オフライン状態の識別がクリティカルなサービスの場合に有効です。 もう一つは、API Versioning - O'Reilly Broadcastでも紹介されていますが、毎回HTTP Headerにapi_versionを埋め込む方法です。
X-Api-Version: 1このやり方は、データベースのリモートバックアップサービスDumperで採用していますが、毎回送るべき情報が少なく、オンライン・オフラインの区別がほとんどないサービスに有効です。OAuthベースのシステムとも親和性が高いでしょう。
リクエスト・コンテキスト
さて、なんらかの方法でAPI versionがリクエストにのってサーバに受け渡されたとして、それを使って分岐する可能性のある場所は、コントローラに限りません。データベースの構造が変わった場合や、JSONの構造が変わった場合など、モデルやその他のクラスでもAPI versionを参照する必要がでてきます。 そこで、リクエスト単位をライフサイクルとするスレッドローカルな変数を使い、コードのどこからでもAPI versionにアクセスできるようにします。 具体的にRailsのコードで見ていきましょう。まずは、﹁APIというコンテキスト﹂を扱うオブジェクトを定義します。ここではOpenStructを使って、ApiContext.api_versi
on = 1
のような形でアサインすると、自動でスレッドローカルに保存してくれるようにします。以下のコードをlib/api_con
text.rb
に置きます。
require 'ostruct' class ApiContext class << self def method_missing(method_name, *args) Thread.current[:api_context] ||= OpenStruct.new Thread.current[:api_context].send(method_name, *args) end endそして、app/controllers/application_controller.rb︵あるいはapp/controllers/api/base_controller.rb︶に
class ApplicationController < ActionController::Base before_action :clear_context_variables def clear_context_variables Thread.current[:api_context] = nil end endのようにしてグローバルなbefore_actionを定義してやり、リクエスト毎に必ずスレッドローカル変数がクリアされるようにしておき、
ApiContext.api_version = request.headers['x-api-version'].to_iのような形でアサインします。以降はこのApiContext.api_versionを参照して、コントローラやモデルなど、どこにでも分岐を記述できるようになり、
if ApiContext.api_version > 1 { result: [{ name: 'foo', age: 20 }] } else { result: { name: 'foo', age: 20 } } endのように局所的に分岐できるようになります。 ところで余談ですが、スレッドローカル変数はいわゆる︵スレッドセーフな︶グローバル変数なので、条件反射的に﹁使うべきではない﹂という反応をする人たちがいます。しかしRails本体でも、たとえばi18nでリクエストごとの言語ロケールをセットするところなどはスレッドローカルで実装されており、リクエストをライフサイクルとする広域変数にスレッドローカル変数を使うのはむしろ定番です。 ただし、当然ながら副作用としてスコープリークが発生し、MVCの分離が甘くなるので、ApiContextの使用は必要最小限にとどめ、テストの記述にも注意が必要です。