hfunaiの記事に便乗。
http://blog.monospace.jp/2010/10/31/django_piston_intro/
django-pistonを使うとDjangoで簡単にWebAPIを作れます。
jespern / django-piston / wiki / Home — Bitbucket
fields/excludeにフィールド名を指定すると、必要なフィールドのみを出力したりできます。
ただ、APIで提供するデータの構造とモデルの構造が一対一の対応にならない場合、辞書を頑張って生成するか、ハンドラのフックポイントを実装するか、Emitterクラスをゴリゴリ書くなど、ちょっと面倒です。
そういう場合に、bpmappersと組み合わせるといいかも、という話です。
今回はGETでデータを取得(参照)する場合のみを想定してます。保存する場合はFormで頑張ろう・・・。
bpmappersについて
bpmappersはモデルを辞書へのデータのマッピングを支援するモジュールです。
bpmappers ドキュメント - サイトは移転しました
BeProudでも結構使ってます。
ふつうにpistonを使ってみる
まずは普通にpistonを使った場合。
myapp/models.py
from django.db import models class Person(models.Model): name = models.CharField(max_length=20) class Book(models.Model): title = models.CharField(max_length=20) author = models.ForeignKey(Person)
こんなモデルを書いて、適当にfixtureでデータを入れておきます。
myapp/fixtures/test.json
[ { "pk": 1, "model": "myapp.person", "fields": { "name": "monjudoh" } }, { "pk": 2, "model": "myapp.person", "fields": { "name": "wozozo" } }, { "pk": 1, "model": "myapp.book", "fields": { "author": 2, "title": "python book" } }, { "pk": 2, "model": "myapp.book", "fields": { "author": 1, "title": "js book" } } ]
api/urls.py
from django.conf.urls.defaults import * from piston.resource import Resource from api.handlers import BookHandler book_handler = Resource(BookHandler) urlpatterns = patterns('', url(r'^book/(?P<object_id>[^/]+)/', book_handler), )
api/handlers.py
from piston.handler import BaseHandler from myapp.models import Person, Book class BookHandler(BaseHandler): allowed_methods = ('GET',) fields = ('title', ('author', ('id', 'name'))) model = Book def read(self, request, object_id): return Book.objects.get(pk=object_id)
厳密にはエラー処理なども必要ですが、省略してます。authorの下に "_state" というのが出てしまうのでfieldsを指定しています。
ディレクトリ構造はこんな感じ
$ tree myproject myproject |-- __init__.py |-- api | |-- __init__.py | |-- handlers.py | |-- models.py | |-- urls.py | `-- views.py |-- manage.py |-- myapp | |-- __init__.py | |-- fixtures | | `-- test.json | |-- models.py | `-- views.py |-- settings.py |-- test.db `-- urls.py
/api/book/1/ の出力は次のようになります。
{ "author": { "name": "wozozo", "id": 2 }, "title": "python book" }
モデルのプロパティがそのままマッピングされて出力されています。さてここまではいいのですが...
最終的にほしかった出力結果はこうではなく、キー名が変更された以下のような形でした。
{ "book_title": "python book", "author_info": { "author_name": "wozozo", "author_id": 2 } }
さて対応してみましょう。先ほどのBookHandlerを書き換えました。
api/handlers.py
from piston.handler import BaseHandler from myapp.models import Person, Book class BookHandler(BaseHandler): allowed_methods = ('GET',) def read(self, request, object_id): obj = Book.objects.get(pk=object_id) return { 'book_title': obj.title, 'author_info': { 'author_id': obj.author.id, 'author_name': obj.author.name, }, }
辞書でマッピングすることで解決できました。
実際に扱うシステムでは、もっと情報量が多く、似た様なAPIがあったりすることもあります。そうなると、このマッピング部分のメンテナンスやコードの再利用のコストが増えてきます。
そこでbpmappersを使います。
bpmappersを使ってマッピングしてみる
api/mappers.pyを新たに作成し、api/handlers.pyを書き換えました。
api/mappers.py
from bpmappers import Mapper, RawField, DelegateField class AuthorMapper(Mapper): author_id = RawField('id') author_name = RawField('name') class BookMapper(Mapper): book_title = RawField('title') author_info = DelegateField(AuthorMapper, 'author')
api/handlers.py
from piston.handler import BaseHandler from myapp.models import Person, Book from api.mappers import BookMapper class BookHandler(BaseHandler): allowed_methods = ('GET',) def read(self, request, object_id): obj = Book.objects.get(pk=object_id) return BookMapper(obj).as_dict()
これでデータのマッピングのみをmappers.pyに切り出せました。
AuthorMapperやBookMapperを他の部分で再利用することも簡単です。
複雑な辞書を何種類も扱っていて、「似てるけどちょっとだけ違う構造にしたい」といった場合にはbpmappersは大活躍してます。