Django REST Frameworkを使って爆速でAPIを実装する

  • 445
    いいね
  • 6
    コメント

:christmas_tree: この記事は「Python その2」Advent Calendar 2015の14日目です。

何を書こうか迷ってましたが、今年学んだPython関係の技術で一番恩恵を預かったREST Frameworkについて書いてみようと思います。

:golf: はじめに

APIを作るのは結構面倒

初めにAPIを作るということの難易度に触れておこうと思います。
実際に作った経験のある方も多いと思いますが、WebAPIはWebページと違ってロジックのみに集中して開発できるので、開発しやすいといえば開発しやすいです。デザインや利用環境気にしたり、HTMLやJavascriptような複数の言語を駆使する必要も全く無いです。レスポンスを正しく受け取って適切にレスポンスを返すだけ。

とはいうものの実際作ってみると面倒な事が多く出てきます。例えば入力された値が正しいのかどうかをチェックしたりだとか、複数の条件で結果をフィルタリングしたりだとか、返すアイテムの件数を分割したりだとか。
またモデルの数が多いと単純に作らなくてはならないAPIの数も多くなります。これらを一つ一つ作っていくとなると相当な労力がかかりますね。

Django REST Frameworkを使おう

そこで今回の主題であるDjango REST Frameworkの登場です。Django自身の機能と合わせて作ることで、何もない状態から爆速でAPI作成までを実現できます。そして嬉しいことにAPIを試すためのWEBコンソール付きです。APIでは正しいパラメータを自分で補完して入力する必要があるので動作確認がなにかと面倒ですが、Django REST Frameworkであればその心配もありません。これはもう活用するしか無いですね。

:beginner: 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
    • 特定のエントリーを置き換える
  • DELTE /entries/$entry_id
    • 特定のエントリーを削除する

みたいな形でAPIのエンドポイントを設計します。

POSTとPUTの違いは、特定のリソースに対しての操作かどうかの違いで使い分けるみたいです。新規に記事を投稿するときはまだentry_idが振られていないので /entriesに対してPOST、記事作成後の更新に対してはentry_idが振られているのでPUTで操作するといった感じです。

:cat: とりあえず作ってみる

では早速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

モデルの定義

blog/models.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')

とりあえずこんな形でModelを定義しておきます。実際にブログを作るならもうちょっと制約を加えたりとかありますが、今回はAPIを作って試すだけなのでこのくらいにしておきます。

名前とメールアドレスだけを持ったUserと、ブログの記事を表すEntryを定義しています。Entryからブログの書いた人の情報としてForeignKeyでUserを参照しています。Djangoの場合related_nameを付けるだけで逆参照も行えるので便利です。

データベース構築
Modelができたらデータベースを構築します。今回は準備がいらないSQlite3を使います。

django_rest_framework_test/settings.py

# 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の管理画面に入ることが出来ます。


image


この管理画面ではDjangoのモデルをWeb上で操作することが出来ます。ただ先ほど作成したEntryUserが無いことが分かります。(紛らわしいですがAUTHENTICATION AND AUTHORIZATIONのUsersはDjangoのログインを管理するためのものです。)

ここに自分で定義したモデルを追加するには少しソースに付け足す必要があります。

blog/admin.py
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の表示を細かくカスタマイズしたりも出来ますが長くなるので割愛します。


image


python manage.py runserver

を再度実行してブラウザで開くと先ほど定義したモデルが出てきます。(DEBUGフラグがTrueになっていれば、ソース変更のたびにrunserverを実行し直す必要はありません)


image


Entryの作成・編集がここで一通り出来てしまいます。後で一覧のAPIを試すために何個かEntryを作っておくと便利です。

Djagno REST Frameworkを組み込む

さて、長くなりましたがここまでは通常のDjangoの流れと同じです。
ここからREST APIの構築へと入っていきましょう。

REST Frameworkの読み込み

django_rest_framework_test/settings.py
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の定義

blog/serializer.py
# 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の定義

blog/views.py
# 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定義

django_rest_framework_test/urls.py
# 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)),
]
blog/urls.py
# 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/にアクセスしてみます。


image


するとこのようなAPIコンソールが出現します。コンソールを使用することでJSONを見やすく表示できたり、APIをテストするためのフォームを自前で用意する必要がなくなりますのでどんどん活用しましょう。
ちなみにコマンドでcurl http://localhost:8000/api/みたいに叩いた時は、コンソールのHTMLではなくちゃんとJSON形式で返るのでご安心を。?format=jsonを付けることでブラウザでも強制JSONにすることも出来ます。


image


http://localhost:8000/api/entries/ に移動するとこんな感じになります。エントリー一覧が取得できるエンドポイントです。先ほど管理画面で入れたデータが表示されているのが分かります。

また画面下部にはフォームが付属しており、ここから新規EntryのPOSTをテストすることができます。choicesを利用している場合可読性の高いラベルの方で選択できたり、リレーションモデルの場合も存在するアイテムから選択できるようになっているので手間がかからないです。

Authorの選択肢が全部「User Object」になってしまっていますが、これはUserModelの __str__ を適切に上書きしてあげることで識別できるようになります。

blog/model.py
class User(models.Model):
    
    def __repr__(self):
        # 主キーとnameを表示させて見やすくする
        # ex) 1: Alice
        return "{}: {}".format(self.pk, self.name)

    __str__ = __repr__  # __str__にも同じ関数を適用

image


http://localhost:8000/api/entries/1 に移動するとこのような形になります。今度は個別の要素に対するGETなので、pk=1のEntryが1件表示されます。また右上にはDELETEボタンが、フォームには今の値が設定されてPOSTの代わりにPUTでボタンが設置されています。これを使うことでEntryの削除や更新が可能です。

:rocket: 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の部分だけを上書きしてあげればよいです。

blog/serializer.py

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回のリクエストで取得できる件数を制限し、必要に応じて次ページ分のデータを取得できるようにします。

django_rest_framework_test/settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 2
}

今までDjangoREST用の設定は追加してませんでしたが、ペジネーションを利用するには専用の設定を追加する必要があります。

REST_FRAMEWORKという辞書を定義して、DEFAULT_PAGINATION_CLASSPAGE_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を絞り込みたいとかそういう時用ですね。

django_rest_framework_test/settings.py
INSTALLED_APP = [
...
    'blog',
    'rest_framework'
    'django_filters',  # 追加
]

# 追記
REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',) 
}

設定ファイルにdjango_filtersDEFAULT_FILTER_BACKENDSを追加します。ペジネーションも利用する場合はよしなに辞書を統合してください。

class EntryViewSet(viewsets.ModelViewSet):
    queryset = Entry.objects.all()
    serializer_class = EntrySerializer
    filter_fields = ('author', 'status')

そしてfilterしたいViewSetにfilter_fieldsを追加します。これでauthorstatusがフィルター出来るようになりました。

APIの後ろに?author=1のようにつければUserのid=1が書いた記事のみがとれるようになります。また?status=publicのようにすれば公開のステータスの記事のみが取れます。

http://www.django-rest-framework.org/api-guide/filtering/
他にも検索だったり、独自の指定方法ができたりも可能です。

:white_flower: まとめ

Django REST APIを使うと少ない労力でしっかりとしたAPIを作ることが出来ます。データベースからAPIの出力までここまで面倒を見てくれるフレームワークはそこまで無いんじゃないでしょうか。DjangoなのでAPI以外にもページを作りたいといった用途にも問題なく対応できます。是非Django REST API Frameworkを使ってみてください。(そして日本語での知見を貯めてきましょう!)

本当はもっと細かいカスタマイズにも触れたかったのですが、時間に追われて後半駆け足になってしまいました。シリアライズとかフィルタリングとかそれだけで数記事書けそうなボリュームがあるので、またいつか独立した記事作って補完したいと思います。

:octocat: ソース

https://github.com/pistatium/django_rest_framework_test
今回作ったサンプルのソースを置いておきます。
ディレクトリとかよくわからなくなった時にでもどうぞ。

:book: 参考URL


明日は@satoshi03さんです。