python / django / モデルメッソドをAPI化する

今風のウェブアプリを作ろうとするとフォームに状態を蓄積してそれを一気にポストして処理するバッチ更新から個々のモデルオブジェクトをajaxを使って更新するスタイルになりがちだ。するとモデル毎にAPIを設定しなくてはいけなくなる。これは面倒だ。そこで、モデルのメソッドをAPI化しちゃおう。

まず、オブジェクトの住み家のURLを決める。
/model/{ClassName}/{id}/としよう。
これは「クラスデコレータでモデルオブジェクトにURLを与える」を参照。
モデルインスタンスに対するAPIはREST的にこのURLかその下にする。

モデル側ではAPIコールに対応するメソッドを定義する。


clas Post(Model):
def api_publish(self):
""" ブログの記事を公開する """
...

/model/Blog/9/publish/にPOSTすると記事が公開されるようにする。
(/model/Blog/9/にoperatin=publishとした方がよりRESTyなのかもしれない…)

要は上のURLとHTTPのパラメタとこのメソッドを継げばいい。
以下のようなviewハンドラを導入すれば、クラス毎にハンドラを定義しなくてもすむ。

URL


# モデル用URL /m/クラス名/ID/
(r'^model/(.*?)/(\d+)/(.*)/$', 'main.api.generic_handler'),

ハンドラ


name_to_class=dict([(c.__name__.lower(),c)
for c in model_classes_in_module(main.models)])

def generic_handler(request, class_name, id_str, op_name=None):
""" APIコールからモデルのメソッドを呼ぶgenericなハンドラ。
(クラス名、ID、メソッド名)をbound-methodに解決する。
それをHTTPリクエストパラメタを引数として呼出す。
"""
# クラス名ーー>クラスオブジェクト
try:
cls=name_to_class[class_name.lower()]
except ValueError:
return HttpResponseNotFound(class_name)

# クラス+IDーー>インスタンス
obj=get_object_or_404(cls, pk=int(id_str))

# インスタンス+メソッド名ーー>bound-method
# メソッド名は「api_」で始まるものだけを呼出せるようにする。
# そうしないと、ネットワークを経て何でも呼出せちゃうから。
bound_method=getattr(obj, 'api_'+op_name)

# クエリパラメタを**kw引数にしてメソッドを呼ぶ
return bound_method(request, **dict(request.REQUEST.items()))

model_classes_in_module()は「モジュール内のモデルクラスを探し出す関数」を参照。
APIメソッドを完成させるとこういう風かな。


class Post(Model):
def api_publish(self):
""" ブログの記事を公開する """

# やるべきことをする
self.publish()

# 返す値は呼出し方によって違う。
# ajaxコールの場合。クエリパラメタでクライアントが選べるようにしておく。
# これもデコレータで簡略化したほうがいいな…
from django.http import *
if kw.get('request_type','')=='ajax':
# 自分をJSON化したものを返すのがいいのかもしれない、、
return HttpResponse("ok")

# フォームの場合: 元のところに戻る
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/))

オブジェクトのURLからハンドラまでの経路が自動化できたので、あとはメソッドを定義するだけでAPI化できる。

最後に注意事項: 「api_」接頭辞のメソッドに限られているとはいえ、ネットのインプットからクラスとメソッドを選択して呼んでいるので、セキュリティーの問題があるかもしれない。自己責任で使ってください。