この記事は「Python その2」Advent Calendar 2015の14日目です。
何を書こうか迷ってましたが、今年学んだPython関係の技術で一番恩恵を預かったREST Frameworkについて書いてみようと思います。
はじめに
APIを作るのは結構面倒
初めにAPIを作るということの難易度に触れておこうと思います。
実際に作った経験のある方も多いと思いますが、WebAPIはWebページと違ってロジックのみに集中して開発できるので、開発しやすいといえば開発しやすいです。デザインや利用環境気にしたり、HTMLやJavascriptような複数の言語を駆使する必要も全く無いです。レスポンスを正しく受け取って適切にレスポンスを返すだけ。
とはいうものの実際作ってみると面倒な事が多く出てきます。例えば入力された値が正しいのかどうかをチェックしたりだとか、複数の条件で結果をフィルタリングしたりだとか、返すアイテムの件数を分割したりだとか。
またモデルの数が多いと単純に作らなくてはならないAPIの数も多くなります。これらを一つ一つ作っていくとなると相当な労力がかかりますね。
Django REST Frameworkを使おう
そこで今回の主題であるDjango REST Frameworkの登場です。Django自身の機能と合わせて作ることで、何もない状態から爆速でAPI作成までを実現できます。そして嬉しいことにAPIを試すためのWEBコンソール付きです。APIでは正しいパラメータを自分で補完して入力する必要があるので動作確認がなにかと面倒ですが、Django REST Frameworkであればその心配もありません。これはもう活用するしか無いですね。
RESTfulなAPIとは
Django REST Frameworkを100%活かすためには、RESTfulなAPIについて知っておく必要があります。本題ではないので詳しくは書きませんが簡単におさらいしておきます。
WebAPI ≠ RESTful
HTTP/HTTPSを介してやり取りするAPI全てがRESTfulであるというのは誤解です。RESTfulとはAPIを作る上での設計ルールみたいなもので、これに則ってAPIを作成することでシンプルかつ見通しの良いAPIを作ることが出来ます。
モデルと1対1のAPI
RESTful APIの特徴はモデルと1対1で結びつくようにAPIを作っていく点ですね。1つのAPI呼び出しであれこれモデルを参照してほしい結果をまるごと取ってくるのではなく、1モデル内のリソースに対して一覧を問い合わせたり、個々に対して追加・更新・削除を行うのがRESTfulのイメージです。
モバイルアプリ向けに少ないリクエストで情報を取りたいだとか、複数のモデルをトランザクションを貼って操作するような用途には適してませんが、ルールが明快で各APIの独立性が高いので非常に扱いやすいです。
GET, POST, PUT, DELETE
RESTfulの代表格とも言える4つのHTTPメソッドです。
ブログのエントリー(entry)に対してのAPIの例を見てみると
-
GET /entries
- エントリー一覧を取得する
-
POST /entries
- エントリーを追加する
-
GET /entries/$entry_id
- 特定のエントリーを取得する
-
PUT /entries/$entry_id
- 特定のエントリーを置き換える
-
DELETE /entries/$entry_id
- 特定のエントリーを削除する
みたいな形でAPIのエンドポイントを設計します。
POSTとPUTの違いは、特定のリソースに対しての操作かどうかの違いで使い分けるみたいです。新規に記事を投稿するときはまだentry_idが振られていないので /entries
に対してPOST、記事作成後の更新に対してはentry_idが振られているのでPUTで操作するといった感じです。
とりあえず作ってみる
では早速Django REST Frameworkを使ってみましょう。
作るもの
ブログの例を挙げたので、そのままブログのEntryを操作するAPIを例として作ってみましょう。
リレーションモデルも紹介したいのでUserというEntryの所有者を表すモデルも考えます。
確認環境
- OS: OS X(El Capitan)
- Python:
Python 3.5
- Django: 1.9
- django-filter: 0.11.0
- djangorestframework: 3.3.1
今回はPython3で動作確認してます。
特に固有の文法を使ってないですし、Django REST FrameworkもPython 2.7をサポートしているのでPython2でも動くと思いますが、2020年にサポートが切れてしまいますし、2015年も終わろうとしている今、積極的にPython3へ切り替えて行くほうがいいと思います。
プロジェクトセットアップ
必要なライブラリのインストール
pip install django
pip install djangorestframework
pip install django-filter
プロジェクト・アプリの作成
# django_rest_framework_testという名前でプロジェクトを作ってみます
django-admin startproject django_rest_framework_test
cd django_rest_framework_test/
python manage.py startapp blog
# ディレクトリ構造確認
.
└── django_rest_framework_test
├── blog
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── django_rest_framework_test
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
モデルの定義
from django.db import models
class User(models.Model):
name = models.CharField(max_length=32)
mail = models.EmailField()
class Entry(models.Model):
STATUS_DRAFT = "draft"
STATUS_PUBLIC = "public"
STATUS_SET = (
(STATUS_DRAFT, "下書き"),
(STATUS_PUBLIC, "公開中"),
)
title = models.CharField(max_length=128)
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
status = models.CharField(choices=STATUS_SET, default=STATUS_DRAFT, max_length=8)
author = models.ForeignKey(User, related_name='entries', on_delete=models.CASCADE)
とりあえずこんな形でModelを定義しておきます。実際にブログを作るならもうちょっと制約を加えたりとかありますが、今回はAPIを作って試すだけなのでこのくらいにしておきます。
名前とメールアドレスだけを持ったUser
と、ブログの記事を表すEntry
を定義しています。Entry
からブログの書いた人の情報としてForeignKeyでUser
を参照しています。Djangoの場合related_nameを付けるだけで逆参照も行えるので便利です。
データベース構築
Modelができたらデータベースを構築します。今回は準備がいらないSQlite3を使います。
…
# blogを追記
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog',
]
…
Djangoの設定にいま作成したblog appを登録し、下記コマンドを叩きます。
# migrationファイルを作る
python manage.py makemigrations
# migrationファイルを元にDBに反映する
python manage.py migrate
これでmanage.pyと同じ階層にdb.sqlite3というデータベースのファイルが作られ、blogのモデルに応じたテーブルも自動で作成されます。SQLを一切書かずにデータベースを用意出来てしまうのでこの機能は本当便利です。本番用にMySQLやPostgresに差し替える時もsettingsだけを変えるだけで対応可能です。
動作確認
REST APIを組み込む前に簡単に動作確認をしてみます。
# admin用のユーザー作成
python manage.py createsuperuser
Username (leave blank to use 'kimihiro_n'): dev
Email address:
Password:
Password (again):
Superuser created successfully.
# 開発サーバーを起動
python manage.py runserver
http://localhost:8000/admin
でDjangoの管理画面に入ることが出来ます。
この管理画面ではDjangoのモデルをWeb上で操作することが出来ます。ただ先ほど作成したEntry
とUser
が無いことが分かります。(紛らわしいですがAUTHENTICATION AND AUTHORIZATIONのUsersはDjangoのログインを管理するためのものです。)
ここに自分で定義したモデルを追加するには少しソースに付け足す必要があります。
from django.contrib import admin
from .models import User, Entry
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
pass
@admin.register(Entry)
class Entry(admin.ModelAdmin):
pass
blog内にあるadmin.pyを開いて上記のように書き直します。Adminの表示を細かくカスタマイズしたりも出来ますが長くなるので割愛します。
python manage.py runserver
を再度実行してブラウザで開くと先ほど定義したモデルが出てきます。(DEBUGフラグがTrueになっていれば、ソース変更のたびにrunserverを実行し直す必要はありません)
Entryの作成・編集がここで一通り出来てしまいます。後で一覧のAPIを試すために何個かEntryを作っておくと便利です。
Djagno REST Frameworkを組み込む
さて、長くなりましたがここまでは通常のDjangoの流れと同じです。
ここからREST APIの構築へと入っていきましょう。
REST Frameworkの読み込み
INSTALLED_APPS = (
...
'blog',
'rest_framework',
)
設定ファイルのINSTALLED_APPSにrest_frameworkを追記します。これでDjangoからREST Frameworkを呼び出せるようになりました。
REST APIを作成するには最低限以下の3つを定義する必要があります。
- Serializer
- ViewSet
- URL pattern
ざっくり言うと、Serializerは「Modelをどのようにシリアライズ(・デシリアライズ)するかを決めるためのもの」、ViewSetは「APIのクエリーをどう解釈するかを決めるためのもの」、そしてURL Patternは「DjangoにURLのパターンを教えるためのもの」です。これらをAPI化したいModelに対してそれぞれ定義していきます。
最低限のAPI実装をしたい時には結構手間に感じるかもしれませんが、このように分割することによって高い拡張性とコードの見通しのよさを実現しています。
Serializerの定義
# coding: utf-8
from rest_framework import serializers
from .models import User, Entry
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('name', 'mail')
class EntrySerializer(serializers.ModelSerializer):
class Meta:
model = Entry
fields = ('title', 'body', 'created_at', 'status', 'author')
これがSerializerのミニマムな定義です。serializers.ModelSerializer
を継承してModelに対応するSerializerを作成します。 fields
に与えるのはAPIとして出力したいフィールド名のタプルです。
filedsに列挙したものはデフォルトの実装でシリアライズして出力されます。例えばEntrySerializerではリレーションであるauthor
フィールドを出力しようとしていますが、デフォルトではauthorのid(=pk)が出力されます。Serializerをカスタマイズすることによってこれらの挙動を変更することができます(後述)。
ViewSetの定義
# coding: utf-8
import django_filters
from rest_framework import viewsets, filters
from .models import User, Entry
from .serializer import UserSerializer, EntrySerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class EntryViewSet(viewsets.ModelViewSet):
queryset = Entry.objects.all()
serializer_class = EntrySerializer
ViewSetはこのような形になります。queryset
にDjangoのModelのクエリセットを、serializer_class
に先ほど定義したSerializerを指定します。querysetはあらかじめフィルタリングすることも可能です。
URL pattern定義
# coding: utf-8
from django.conf.urls import url, include
from django.contrib import admin
from blog.urls import router as blog_router
urlpatterns = [
url(r'^admin/', admin.site.urls),
# blog.urlsをincludeする
url(r'^api/', include(blog_router.urls)),
]
# coding: utf-8
from rest_framework import routers
from .views import UserViewSet, EntryViewSet
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
router.register(r'entries', EntryViewSet)
最後にURL patternの定義です。
routerというものを使ってModel毎に登録していきます。
上記のように設定した場合、/api/
がREST APIへの入り口となっており
GET /api/users/
でUserの一覧、
GET /api/entries/
でEntryの一覧
へとアクセスすることが出来ます。もちろんPOSTやIDを指定した操作も同時に登録されます。
API動作確認
これでREST APIが動かせる状態になったので動かしてみましょう。
python manage.py runserver
でサーバーを起動させ、http://localhost:8000/api/
にアクセスしてみます。
するとこのようなAPIコンソールが出現します。コンソールを使用することでJSONを見やすく表示できたり、APIをテストするためのフォームを自前で用意する必要がなくなりますのでどんどん活用しましょう。
ちなみにコマンドでcurl http://localhost:8000/api/
みたいに叩いた時は、コンソールのHTMLではなくちゃんとJSON形式で返るのでご安心を。?format=json
を付けることでブラウザでも強制JSONにすることも出来ます。
http://localhost:8000/api/entries/
に移動するとこんな感じになります。エントリー一覧が取得できるエンドポイントです。先ほど管理画面で入れたデータが表示されているのが分かります。
また画面下部にはフォームが付属しており、ここから新規EntryのPOSTをテストすることができます。choicesを利用している場合可読性の高いラベルの方で選択できたり、リレーションモデルの場合も存在するアイテムから選択できるようになっているので手間がかからないです。
Authorの選択肢が全部「User Object」になってしまっていますが、これはUserModelの __str__
を適切に上書きしてあげることで識別できるようになります。
class User(models.Model):
…
def __repr__(self):
# 主キーとnameを表示させて見やすくする
# ex) 1: Alice
return "{}: {}".format(self.pk, self.name)
__str__ = __repr__ # __str__にも同じ関数を適用
http://localhost:8000/api/entries/1
に移動するとこのような形になります。今度は個別の要素に対するGETなので、pk=1のEntryが1件表示されます。また右上にはDELETEボタンが、フォームには今の値が設定されてPOSTの代わりにPUTでボタンが設置されています。これを使うことでEntryの削除や更新が可能です。
APIをカスタマイズする
基本的なAPIの実装・使い方は上記で終わりですが、実際運用して使うにはフィルタリングだったり出力形式のカスタマイズだったりを行う必要が出てきます。今回はその中からいくつかカスタマイズ方法をピックアップして紹介します。載せなかったカスタマイズについては公式ドキュメント(英語)を見てもらえると。拡張性が高いだけあって様々な要求に答えられるようなシステムになってます。
リレーションモデルを展開する
先ほどの実装だとEntryのJSONを見てみると
{
"title": "Hello, Django REST API!!",
"body": "<script>alert(\"hello\");</script>",
"created_at": "2015-12-09T05:59:46.200277Z",
"status": "draft",
"author": 1
}
のようにauthor
がUserモデルのIDで表示されていました。Entryの中身とそのUserの名前を利用したい場合、まずEntryを取得し、次にauthorのIDを見てGET /api/users/1
を再度リクエストしないとユーザーの名前を取ることが出来ません。これでは実運用上非効率ですよね。Entryを取得する際にUserの中身もJSONに展開して取れると都合がいいです。
{
"title": "Hello, Django REST API!!",
"body": "<script>alert(\"hello\");</script>",
"created_at": "2015-12-09T05:59:46.200277Z",
"status": "draft",
"author": {
"name": "Alice",
"mail": "alice@example.com"
}
}
具体的に言うならこう。これならAPIを扱う側からしても使いやすいですね。
APIのレスポンスをこのように変えるにはSerializerでauthorの部分だけを上書きしてあげればよいです。
…
class EntrySerializer(serializers.ModelSerializer):
# authorのserializerを上書き
author = UserSerializer()
class Meta:
model = Entry
fields = ('title', 'body', 'created_at', 'status', 'author')
EntrySerializerにauthorフィールドを定義し、そこで自分で定義したUserSerializerをセットしてあげるだけで対応することが出来ます。read_only
をつけるかは好みですが、EntryをPOST,PUTするときにAPI仕様がややこしくなるのでEntryからは上書きできないようにしたほうがいいかもしれません。あと、UserSerializerのfieldsにid
を追加してあげるとより親切です。
http://www.django-rest-framework.org/api-guide/serializers/
他にもSerializerを上書きすることで、表示形式を変えたりバリデーションを設けたり出来ます。
ペジネーション
次にペジネーションについて。これまでの実装だと、Modelを全件取ってきて表示する仕様になってます。データが少ないうちは問題無いですが、件数が溜まってくるにつれ負荷も転送量もひどいことになってきます。そこでWebページで行われているように、1回のリクエストで取得できる件数を制限し、必要に応じて次ページ分のデータを取得できるようにします。
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 2
}
今までDjangoREST用の設定は追加してませんでしたが、ペジネーションを利用するには専用の設定を追加する必要があります。
REST_FRAMEWORKという辞書を定義して、DEFAULT_PAGINATION_CLASS
とPAGE_SIZE
を追加してみましょう。
これだけでAPIがoffset
(開始位置)とlimit
(一度の取得上限数、default=PAGE_SIZE)パラメータに対応できます。
{
"count": 4,
"next": "http://localhost:8000/api/entries/?limit=2&offset=2",
"previous": null,
"results": [
{
"id": 1,
"title": "Hello, Django REST Framework!!",
"body": "Hello!",
"created_at": "2015-12-12T11:55:22.310203Z",
"status": "draft",
"author": 1
},
{
"id": 2,
"title": "The Zen of Python",
"body": "The Zen of Python, by Tim Peters\r\n\r\nBeautiful is better than ugly.\r\nExplicit is better than implicit.\r\nSimple is better than complex.\r\nComplex is better than complicated.\r\nFlat is better than nested.\r\nSparse is better than dense.\r\nReadability counts.\r\nSpecial cases aren't special enough to break the rules.\r\nAlthough practicality beats purity.\r\nErrors should never pass silently.\r\nUnless explicitly silenced.\r\nIn the face of ambiguity, refuse the temptation to guess.\r\nThere should be one-- and preferably only one --obvious way to do it.\r\nAlthough that way may not be obvious at first unless you're Dutch.\r\nNow is better than never.\r\nAlthough never is often better than *right* now.\r\nIf the implementation is hard to explain, it's a bad idea.\r\nIf the implementation is easy to explain, it may be a good idea.\r\nNamespaces are one honking great idea -- let's do more of those!",
"created_at": "2015-12-12T11:56:32.854278Z",
"status": "draft",
"author": 2
}
]
}
next
,previous
で前後のリクエストのURLが分かるのも便利ですね。
ちなみにlimitを固定したpage
によるリクエストや、オブジェクトのIDを起点に前後の習得をするcursor
方式も設定を変えるだけで利用することができます。
http://www.django-rest-framework.org/api-guide/pagination/
フィルター
最後にフィルターを紹介。author
でEntryを絞り込みたいとかそういう時用ですね。
INSTALLED_APP = [
...
'blog',
'rest_framework',
'django_filters', # 追加
]
# 追記
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
}
設定ファイルにdjango_filters
とDEFAULT_FILTER_BACKENDS
を追加します。ペジネーションも利用する場合はよしなに辞書を統合してください。
class EntryViewSet(viewsets.ModelViewSet):
queryset = Entry.objects.all()
serializer_class = EntrySerializer
filter_fields = ('author', 'status')
そしてfilterしたいViewSetにfilter_fields
を追加します。これでauthor
とstatus
がフィルター出来るようになりました。
APIの後ろに?author=1
のようにつければUserのid=1が書いた記事のみがとれるようになります。また?status=public
のようにすれば公開のステータスの記事のみが取れます。
http://www.django-rest-framework.org/api-guide/filtering/
他にも検索だったり、独自の指定方法ができたりも可能です。
まとめ
Django REST APIを使うと少ない労力でしっかりとしたAPIを作ることが出来ます。データベースからAPIの出力までここまで面倒を見てくれるフレームワークはそこまで無いんじゃないでしょうか。DjangoなのでAPI以外にもページを作りたいといった用途にも問題なく対応できます。是非Django REST API Frameworkを使ってみてください。(そして日本語での知見を貯めてきましょう!)
本当はもっと細かいカスタマイズにも触れたかったのですが、時間に追われて後半駆け足になってしまいました。シリアライズとかフィルタリングとかそれだけで数記事書けそうなボリュームがあるので、またいつか独立した記事作って補完したいと思います。
ソース
https://github.com/pistatium/django_rest_framework_test
今回作ったサンプルのソースを置いておきます。
ディレクトリとかよくわからなくなった時にでもどうぞ。
参考URL
- http://www.django-rest-framework.org/
- http://www.slideshare.net/unsolublesugar/res-tful
- http://qiita.com/KojiOhki/items/5be98eeae72dca2260bc
明日は@satoshi03さんです。