2019-04-21

[Django REST Framework] View の使い方をまとめてみた

Django REST Framework (以下 DRF といいます) をまとめてみた記事のビュー編です。 細かいところまではカバーしきれていませんが、大まかな使い方はわかると思います。

info

ビューと言っても新しい概念ではなく、Django のビューと概ね同じなので身構える心配はありません。 DRF から提供されているビューなので、 DRF の設定と絡めて定義できる便利なやつです。

シリアライザの記事と同じくらいの長さです。頑張って読もうな。

インストール

ひとまず、手元で試すためには インストールが必要です。

ライブラリのインストールは pip で。 djangorestframework 以外はお好みでどうぞ。 django-rest-framework ではないので注意してください。

$ pip install djangorestframework $ pip install markdown $ pip install django-filter

当然ながら Django もインストールされていることを想定しています。

$ pip install django

settings もいじります。 settings.py 等の設定ファイルの INSTALLED_APPSrest_framework を追加します。

INSTALLED_APPS = ( # : # : 'rest_framework', )

ビューの種類

ビューにはいくつか種類があります。 最初にそれぞれの使い所を抑えておきましょう。

warning
  • 当記事では試しやすさを考慮して全部同じファイル(urls.py)に書いていますが、 実案件でこんな書き方をすると上司にぶっ飛ばされるので注意してください。
  • ビューの動作確認には curl という コマンドを使います。

関数ベースのビュー

お気付きの通り、一番簡単な View は Response を返却する関数型のビューということになります。

最小のビューは第一仮引数に request を受取るように定義するだけです。 URLにパラメータを埋め込む場合は引数を増やしたりします。この辺は Django と同じですね。

from rest_framework.response import Response from rest_framework.decorators import api_view @api_view(['GET', 'POST']) def hello_world(request): if request.method == 'POST': return Response({"message": "Got some data!", "data": request.data}) return Response({"message": "Hello, world!"})
info
  • DRF から 提供されているビュー全てに共通して言えることですが、 通常の Django は HttpResponse というクラスで返していたと思いますが、DRFでは Response というクラスで返却します。

書き方はよくあるDjangoのビューと同じですね。 違うのは api_view というデコレータの有無です。 これを使うことにより、 DRF の設定がビューに引き継がれるようになります。

また 引数 として HTTP メソッド を文字列として指定できます。 この辺は require_http_methods と同じですね。

ちなみに後述するクラス型のビューにはパーミッションやパーサなど複数の設定項目があるのですが、 関数型のビューには同じようにデコレータを使って設定することになります。

本当に単純な機能は関数ベースで書いたほうが逆に可読性がよく保守しやすいかもしれません。 設定項目が増えてきたら見栄えが悪いので後述するクラスベースにしちゃったほうがいいです。

クラスベースのビュー

クラスベースのビューは HTTP メソッドごとの インスタンスメソッドを定義できます。 まさにこれは Django の View の様な感じですね。 まぁ実際に継承してるんですが

また、他にも後述する様々な設定を追加できます。 これは関数だとデコレータで連ねて書く必要があり可読性が悪くなってしまうのでクラスベースビューのメリットであると言えそうです。

クラスベース ビュー は APIView クラスを継承して定義します。 対象のHTTPメソッドがそのまま インスタンスメソッド になります。 DRF 的には ハンドラメソッド と呼ばれるようで、 後述する アクションメソッド とは区別します。

from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import authentication, permissions from django.contrib.auth.models import User class UserNameAPI(APIView): authentication_classes = (authentication.TokenAuthentication,) permission_classes = (permissions.IsAdminUser,) def get(self, request, format=None): usernames = [user.username for user in User.objects.all()] return Response(usernames) def post(self, request): # 普通こんなことはしないが.. users = [User(username=name) for name in request.POST.getlist('name')] User.objects.bulk_create(users) return Response({'succeeded': True})

特定のモデルに紐付かないような処理はクラスベースで記述するのがおすすめと言えるでしょう。

私は、クエリが複雑すぎて queryset じゃ処理しきれないとかで SQLAlchemy で処理した結果を返したい という場合などに APIView を使っています。

ちなみに ジェネリックビュークラス(後述) は この APIView を継承して定義されています。

リファレンス

ジェネリックビュー

Django や Django REST framework のお作法に従うなら便利で、そして種類も多い View です。

このビューはモデルというか クエリセットにひも付くため簡略化した書き方ができるのが特徴です。 場合によっては処理を書かなくてよいことが多々あります。

と思ってリファレンスを見ると種類がめっちゃ多いことに気づきますが、 作成, 一覧, 取得, 削除, 更新 の用途によって使い分ければよいだけの話なので身構えることはありません。

Generic Viewクラスは 特定のレコードに 紐づくか, 紐付かないか で分類されています。

紐付かない
紐づく

手始めに ListAPIView を使ってみましょう。

from rest_framework import generics class UserList(generics.ListCreateAPIView): queryset = User.objects.all() serializer_class = UserSerializer

これだけでユーザ一覧を返却するAPIビューの出来上がりです。 各ユーザレコードは UserSerializer に従ってシリアライズされます。

(シリアライザの説明は Serializer の 使い方 をまとめてみた を参照ください。

内部的な話をすると、ジェネリックビューは クエリセット と密接に結びついた GenericAPIView という基底クラスと、 クエリセット を使ってレスポンスを組み立てるための Mixin クラス を継承して定義されています。

Mixin は レスポンスを返却するためのインスタンスメソッドをハンドラメソッドとは別の名前で提供しており、 これをアクションメソッドと呼ぶそうです。

ジェネリックビューではハンドラメソッドから呼び出される形で実行されます。

以下のように対応しています。

HTTPメソッド
    • アクションメソッド
    • 説明
    • 対応しているクラス
GET
    • retrieve(self, request, pk)
    • レコード(単体)の取得
      • RetrieveAPIView
      • RetrieveUpdateAPIView
      • RetrieveDestroyAPIView
      • RetrieveUpdateDestroyAPIView
GET
    • list(self, request)
    • レコード一覧の取得
      • ListAPIView
      • ListCreateAPIView
POST
    • create(self, request)
    • レコードの作成
      • CreateAPIView
      • ListCreateAPIView
PUT
    • update(self, request, pk)
    • レコードの更新
      • UpdateAPIView
      • RetrieveUpdateAPIView
      • RetrieveUpdateDestroyAPIView
PATCH
    • partial_update(self, request, pk)
    • レコードの部分更新
      • UpdateAPIView
      • RetrieveUpdateAPIView
      • RetrieveUpdateDestroyAPIView
DELETE
    • destroy(self, request, pk)
    • レコードの削除
      • DestroyAPIView
      • RetrieveUpdateDestroyAPIView

リファレンスには明記されていなかったんですが、 処理を変えたいときは多分アクションメソッドをオーバーライドすることになるんじゃないかな。 確証はないです。

リファレンス

ビューセット

ビューセットは 取得, 一覧, 登録, 更新, 削除 をまとめて管理するビューです。 (取得と一覧だけできる ReadOnlyModelViewSet というのもあります)

当たり前の処理は 自分で書かなくても この ViewSet が肩代わりしてくれます。

from rest_framework import viewsets class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer

一つ前に書いた UserList の名前と継承クラスを変えただけです。 これだけで、このビューは 取得, 一覧, 登録, 更新, 削除 ができるようになりました。

ViewSet は ハンドラメソッドでなく アクションメソッドで定義するべきと記述がありました。

ジェネリックビューとの一番の違いはおそらく、 アクション にあると思います。

action デコレータ を使ってインスタンスメソッドを定義することで、パスに機能を追加できます。

説明が難しいですが、百聞は一見にしかずということで以下のように ViewSet を定義することで

class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer pagination_class = MyCursorPagination @action(methods=['get'], detail=False) def fullnames(self, request): return Response([ '{user.first_name} {user.last_name}'.format(user=user) for user in self.get_queryset() ]) @action(methods=['get'], detail=True) def fullname(self, request, pk=None): user = self.get_object() return Response('{user.first_name} {user.last_name}'.format(user=user)) # Router については後述 router = routers.DefaultRouter() router.register(r'users', UserViewSet) urlpatterns = [url(r'^api/', include(router.urls))]

インスタンスメソッド名で URL にアクセスすることで 任意の処理が実行できます。

$ curl -X GET 'http://127.0.0.1:8000/api/users/fullnames/' ["takayuki shimizukawa","takanory suzuki","teruhiko teruya"] $ curl -X GET 'http://127.0.0.1:8000/api/users/1/fullname/' "takayuki shimizukawa" $ curl -X GET 'http://127.0.0.1:8000/api/users/2/fullname/' "takanory suzuki" $ curl -X GET 'http://127.0.0.1:8000/api/users/3/fullname/' "teruhiko teruya"
  • methods は 登録対象の HTTP メソッド
  • detail は 詳細API or 一覧API
    • True の場合は pk 引数が必要

これめっちゃ便利ですね。これは検証してないけど Router で登録したときしか使えないのかな。

リファレンス

個人的に考えた使い分けとしては

  • クエリセットを使わない API は クラスベースビュー(APIView) か 関数ビュー
  • モデルに対する処理をまとめて一つのクラスで定義したい場合は View set
  • それ以外は Generic view

みたいな感じかなぁ。

info
  • ビューは ルーティングのヒモ付をしないとページとして認識されません。

  • urls.pyurlpatterns 変数に登録します。

  • from django.conf.urls import url, include urlpatterns = [ url(r'^hello/$', hello_world), url(r'^user/$', UserList.as_view()), ]
  • Django と同じように関数のビューはそのまま、クラスのビューは as_views() メソッドの結果を url 関数でマッピングすることで登録するできます。

  • ViewSet で定義したビューは次に説明する Router を使って登録することもできます。

info

Router

urls.py に Router というものを使って生成された ルーティングルールを追加していきます。 やること自体は簡単なんですが、残念なことにこれにもいくつかの種類があります。

ここでは DefaultRouter だけ説明します。 SimpleRouterDefaultRouter と大体同じものです。

SimpleRouter の機能に加え DefaultRouter は Router のルート画面にアクセスしたときに API のリンク一覧を見せてくれたり、少し便利です。

じゃあブラウザから直接APIを見たいときはどうするのかというと .json の拡張子をつけたり、クエリストリングとして ?format=json をつけてアクセスすれば良いです。

from rest_framework import routers router = routers.DefaultRouter() router.register(r'users', UserViewSet) urlpatterns = [ url(r'^api/', include(router.urls)), ]

これにより /api/ 配下にビューが登録されます。 The Browsable API (私の場合は localhost:8000/api/) にアクセスすると register で登録されたビューをたどることができるようになります。

できましたね。

warning
  • Router で登録できるのは ViewSet だけです。ジェネリックビューとかを登録しようとすると
  • AttributeError: type object 'UserListView' has no attribute 'get_extra_actions' のようにエラーが発生します。 get_extra_actions メソッドは viewsets.py にしか定義されていません。

Router は 詳細API を users/{pk}/$ のように pk を自動的に付加してURL登録してくれるいい子です。

ここは 実際に触ってみるのが一番早いと思います。

リファレンス

Request

ビュー から参照できる request オブジェクトは 概ね Django と同じですが、少し機能追加されています。

request.GET
  • URLパラメータを解析したものを辞書形式で格納されている
request.POST
  • リクエストボディを解析したものが辞書形式で格納されている。
  • ここに入るのは Form データのみで JSON形式 のパラメータは後述する request.data に格納されるようです。
request.query_params
  • 挙動は request.GET と同じ
  • RESTFramework ではこちらの使用が推奨されている。
  • request.GET だと GET メソッドのときしか取れないように聞こえてしまうということっぽい
request.data
  • POST, PUT, PATCH メソッドのリクエストの場合にリクエストボディを解析したものが辞書形式で格納している。
  • これには request.FILES (添付ファイル) を 含む。
  • おそらく最も利用されるパラメータ属性
リファレンス

次セクションからは ビュー に対する制限や追加機能を説明します。

Authentication

ビューの authentication_classes に指定された Authentication クラスを使って認証が行われます。 Authentication クラスは複数指定でき、最初に指定されたものから順に認証に利用され、成功した時点で認証処理は終了します。

今回は認証をチェックするために以下の View を定義します。

from rest_framework.authentication import SessionAuthentication, BasicAuthentication, TokenAuthentication class CheckView(APIView): authentication_classes = (SessionAuthentication, BasicAuthentication, TokenAuthentication) def get(self, request, format=None): content = {'user': str(request.user), 'auth': str(request.auth)} return Response(content) urlpatterns += [url(r'^check/', CheckView.as_view())]

適切な認証情報を持ってこのビューにアクセスするとユーザ情報と認証情報が返却されます。 まぁただのチェック用です。

$ curl -X GET <http://127.0.0.1:8000/check/> {"user":"AnonymousUser","auth":"None"}

認証が成功すると AnonymousUserusername フィールドの値になります。

BasicAuthentication

DRFが提供する最も単純な認証です。Basic認証と同じように ユーザとパスワード(をBASE64エンコードしたもの) を毎回送信することでユーザの認証をします。

たとえば、ユーザの認証情報が test1 / test だとすると、これらを BASE64 化した dGVzdDE6dGVzdA== を Authentication ヘッダに渡せば認証できます。

$ curl -X GET http://127.0.0.1:8000/check/ -H 'Authorization: Basic dGVzdDE6dGVzdA==' {"user":"test1","auth":"None"}

復号が容易なためセキュリティ的にはあまり推奨できません。 ローカルでの動作確認など閉ざされた目的でのみ利用するようにしてください。

info
  • サーバ側から WWW-Authenticate: Basic realm="api" のようなヘッダを返却することでブラウザから認証ダイアログが起動し 認証情報をブラウザに保存できます。

SessionAuthentication

SessionAuthentication は従来の Django と同じ認証方法、つまり Cookie による認証です。

簡易的にセッションを発行するためのビューを以下のように定義します。

class SessionView(APIView): def get(self, request): # 本来は認証しよう # user = authenticate(request.query_params['username'], request.query_params['password']) user = User.objects.get(username=request.GET['username']) r = login(request, user) return Response({'session': request.session.session_key}) urlpatterns += [url(r'^session/', SessionView.as_view())]

アクセスするとセッションIDが得られます。

$ curl -X GET http://127.0.0.1:8000/session/?username=test1 {"session":"a55jor0z61vzhk8n0t2z71vz2rgyleg1"}

発行された Session ID (Set-Cookieされたものと同じ) を Cookie ヘッダに渡すと

$ curl -X GET http://127.0.0.1:8000/check/ -H 'Cookie: sessionid=a55jor0z61vzhk8n0t2z71vz2rgyleg1;' {"user":"test1","auth":"None"}

認証できましたね。

info
  • SessionAuthentication では 普通の Django と同じように PUT, PATCH, POST, DELETE メソッドで CSRFトークン が必要になります。

TokenAuthentication

TokenAuthentication は DRF が提供する認証で SessionAuthentication に少し似ていますが、Cookie は使いません。

利用するためには settings.py を編集し、

INSTALLED_APPS = ( # : 'rest_framework.authtoken', )

トークンを管理するためのテーブル用のマイグレーションをする必要があります。

$ ./manage.py migrate Operations to perform: Apply all migrations: admin, auth, authtoken, contenttypes, sessions Running migrations: Applying authtoken.0001_initial... OK Applying authtoken.0002_auto_20160226_1747... OK

スキーマはこんな感じになってます。 user_id で一意制約がついてるので、ユーザに対して一つしか発行できません。

sqlite> .schema authtoken_token CREATE TABLE IF NOT EXISTS "authtoken_token" ( "key" varchar(40) NOT NULL PRIMARY KEY, "created" datetime NOT NULL, "user_id" integer NOT NULL UNIQUE REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED );

例えば トークンを発行するためのビューを以下のように定義します。

class TokenView(APIView): def get(self, request): # 本来は認証しよう # user = authenticate(request.query_params['username'], request.query_params['password']) user = User.objects.get(username=request.GET['username']) token, _ = Token.objects.get_or_create(user=user) return Response({'token': str(token)}) urlpatterns += [url(r'^token/', TokenView.as_view())]

ユーザ名を指定してトークンを得て、

$ curl -X GET http://127.0.0.1:8000/token/?username=test2 {"token":"38a7db568458feb9f8d6008805bba3fca7e1ca00"}

Authorization ヘッダに トークンを指定すると

$ curl -X GET http://127.0.0.1:8000/check/ -H 'Authorization: Token 38a7db568458feb9f8d6008805bba3fca7e1ca00' {"user":"test2","auth":"38a7db568458feb9f8d6008805bba3fca7e1ca00"}

認証できました。

Basic Authentication や Session Authentication では None だった auth に値が入っています。 トークン認証の場合は Token モデルのインスタンスが格納されるようです。何に使えるかはよくわからない。

コマンドラインからやる場合は、 drf_create_token を使います。すでに有効なトークンがある場合はそれが返却される模様です。

$ ./manage.py drf_create_token test1 Generated token d2f2eacba404528ff49dd6c998bd02fc0a9b3069 for user test1 # 再生成は -r をつける $ ./manage.py drf_create_token -r test1 Generated token b87175b965cf7d886173fff45eb9cab790d2ae6d for user test1

ドキュメントを読む限りでは このトークンに 有効期間を与えるような設定値は提供していないようです。

DRF認証のサードパーティ製ライブラリがいくつかあるようなので、必要な方はそちらを検討してみてください。

関連ライブラリを増やすとDjangoバージョンアップ等の足かせになることがあるので メンテナンスされているかどうかということも考慮に入れて選択することをオススメします。

トークン認証の特徴はドメインに紐付かないことにあります。

先に説明した2つの認証は認証情報がドメインにひも付き、自動的に送信されるという特性がありました。 これは便利な半面 CSRF という攻撃を受けるリスクを孕んでいます。トークンの場合はクライアント側で明示的に指定されない限り認証情報が送信されないため、CSRFの心配はありません。

また、Cookieと違いドメインの制約がないため外部ドメインに対するリクエストであっても認証情報を渡せます。

info
  • CSRF (Cross Site Request Forgery) とは
  • 攻撃者の誘導によって被害者の端末からリクエストが発行される際、 対象ドメインに紐づくユーザ認証情報(主にCookie)が勝手に送信されることで被害者に紐づく何らかの処理を意図せず行う攻撃のことです。 例えば殺害予告などが被害者のアカウントから行われたりします。
  • Django では CSRFトークン と呼ばれる推測困難な値を Cookie と POSTパラメータ などで送信し、サーバ側で一致確認することで CSRF チェックをしています。
  • フレームワークによっては 毎回 CSRFトークン を発行するものもあります。(ここでは認証トークンとCSRFトークンは異なる意味で使っています)

画面ごとに クラスを指定するのが面倒な場合、settings.py の REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] 対して Authentication クラスのモジュールパスを指定します。

REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', ), }

CustomAuthentication

自分で Authentication クラスを作る場合は Authentication 関連クラスを継承して、認証処理を authenticate メソッドに記述します。

今回は TokenAuthentication クラスを継承してトークン認証に有効期限を設けてみます。

class ExpirationTokenAuthentication(TokenAuthentication): delta = timedelta(seconds=TIMEOUT_SECONDS) def authenticate_credentials(self, key): try: token = Token.objects.get(key=key) except Token.DoesNotExist: raise exceptions.AuthenticationFailed("Invalid Token.") if not token.user.is_active: raise exceptions.AuthenticationFailed("User is not active.") now = timezone.now() if now - token.created > self.delta: token.delete() raise exceptions.AuthenticationFailed("The Token is expired.") token.created = now token.save(update_fields=['created']) return (token.user, token)
info
  • アクセスのたびに Token を保存し直していて効率が悪いので、 Redis などの key-value ストアに逃がすといった方法を検討しても良いでしょう。

  • 下記の例では Django の cache 機能を使って最終アクセス時間を保持しています。

    from datetime import timedelta from django.utils import timezone from django.core.cache import cache from rest_framework.authentication import TokenAuthentication from rest_framework.authtoken.models import Token from rest_framework import exceptions # 60秒で有効期限が切れるなんてネットバンクも真っ青なセキュリティですね! TIMEOUT_SECONDS = 60 class ExpirationTokenAuthentication(TokenAuthentication): delta = timedelta(seconds=TIMEOUT_SECONDS) def authenticate_credentials(self, key): try: token = Token.objects.get(key=key) except Token.DoesNotExist: raise exceptions.AuthenticationFailed("Invalid Token.") if not token.user.is_active: raise exceptions.AuthenticationFailed("User is not active.") now = timezone.now() if now - (cache.get(key) or token.created) > self.delta: token.delete() raise exceptions.AuthenticationFailed("The Token is expired.") cache.set(key, now, TIMEOUT_SECONDS) return (token.user, token)
  • 上記のようにトークンや更新時刻だけなら Redis でもいいんですが、紐づくユーザオブジェクトも含めすべてを Redis で管理しようと思うと、ユーザオブジェクトが更新されず、DBとずれてしまう可能性があります。

  • どうしてもやるという場合は、ユーザ更新のイベントをキャッチして Redis 上の情報も更新するようにしてあげましょう。

リファレンス

Permission

Permission は ユーザのもつ何らかの値によって Viewに対するアクセスを制限する機能です。 Authentication と似ていますが、ユーザの認証より後に実施されます。認証されたユーザの値を用いて処理を行うことが多いです。

一番良く使うのはログインの有無を確認する IsAuthenticated でしょう。 Django の login_required デコレータみたいなもんですね。

ちょうどいいので先程の CheckView を編集して設置してみましょう。

class CheckView(APIView): permission_classes = (IsAuthenticated,) authentication_classes = (SessionAuthentication, BasicAuthentication, TokenAuthentication) def get(self, request, format=None): print(dir(request.auth)) content = {'user': str(request.user), 'auth': str(request.auth)} return Response(content)
$ curl -X GET http://127.0.0.1:8000/check/ {"detail":"Authentication credentials were not provided."}

他には管理者ユーザのみに許可する IsAdminUser なんてのもあります。 詳しくは Permissionドキュメント を参照してください。

カスタマイズするには BasePermission を継承し has_permissionhas_object_permission のいずれかを上書きします。

以下はグループに対する権限を確認するパーミッションです。

class GroupMemberPermission(permissions.BasePermission): def has_permission(self, request, view): # ユーザが何のグループにも属していない場合は False になる return request.user.groups.count() def has_object_permission(self, request, view, obj): # 対象グループに属していない場合は False になる return request.user.groups.filter(id=obj.id).exists()
  • has_permission メソッドは 設定されたビューすべてで呼び出されます。 権限がある場合は真と評価されるオブジェクトを返します。
  • has_object_permission は設定されたビュー、かつ、特定のオブジェクトに対して権限があるかどうかを評価します。 こちらも権限の有無により真偽値を返します。
    • こちらは check_object_permissions メソッドが実行された場合だけ呼び出されることに注意してください。
    • 通常、上記のメソッドは get_object メソッドから自動的に呼び出されますが、 get_object をオーバーライドする場合は 明示的に呼び出す必要があります。

グループの一覧、詳細APIページに対して先程のパーミッションを設定してみます。

class GroupListView(generics.ListAPIView): queryset = Group.objects.filter() permission_classes = (GroupMemberPermission, ) authentication_classes = (CustomTokenAuthentication, ) def list(self, request): groups = self.get_queryset().values('id', 'name') return Response({'groups': groups}) class GroupRetrieveView(generics.RetrieveAPIView): queryset = Group.objects.filter() permission_classes = (GroupMemberPermission, ) authentication_classes = (CustomTokenAuthentication, ) def retrieve(self, request, *args, **kwargs): group = self.get_object() return Response({'group': {'id': group.id}}) urlpatterns += [ url(r'^group/$', GroupListView.as_view()), url(r'^group/(?P<pk>.+)/$', GroupRetrieveView.as_view()), ]

動作確認してみます。 group1, group2 を作り、 group1 には test1 というユーザが所属しているというシナリオでデータを作ります。 group2 には誰も属していません。

>>> from django.contrib.auth.models import User, Group >>> g1, _ = Group.objects.get_or_create(id=1, defaults={'name': 'group1'}) >>> g2, _ = Group.objects.get_or_create(id=2, defaults={'name': 'group2'}) >>> u1 = User.objects.get(id=1) >>> u2 = User.objects.get(id=2) >>> g1.user_set.add(u1) >>> g1.user_set.all() <QuerySet [<User: test1>]>

準備が完了したので、それぞれのユーザに対してリクエストを飛ばしてみます。

test1
  • # トークン取得 $ curl -X GET http://127.0.0.1:8000/token/?username=test1 {"token":"67a1fb9588b9ec9794e02d5addc8743243fecb1a"} # 一覧API $ curl -X GET http://127.0.0.1:8000/group/ -H 'Authorization: Token 67a1fb9588b9ec9794e02d5addc8743243fecb1a' {"groups":[{"id":1,"name":"group1"},{"id":2,"name":"group2"}]} # group1 の詳細API $ curl -X GET http://127.0.0.1:8000/group/1/ -H 'Authorization: Token 67a1fb9588b9ec9794e02d5addc8743243fecb1a' {"group":{"id":1}} # group2 の詳細API $ curl -X GET http://127.0.0.1:8000/group/2/ -H 'Authorization: Token 67a1fb9588b9ec9794e02d5addc8743243fecb1a' {"detail":"You do not have permission to perform this action."}
test2
  • # トークン取得 $ curl -X GET http://127.0.0.1:8000/token/?username=test2 {"token":"38a7db568458feb9f8d6008805bba3fca7e1ca00"} # 一覧API $ curl -X GET http://127.0.0.1:8000/group/ -H 'Authorization: Token 38a7db568458feb9f8d6008805bba3fca7e1ca00' {"detail":"You do not have permission to perform this action."} # group1 の詳細API $ curl -X GET http://127.0.0.1:8000/group/1/ -H 'Authorization: Token 38a7db568458feb9f8d6008805bba3fca7e1ca00' {"detail":"You do not have permission to perform this action."} # group2 の詳細API $ curl -X GET http://127.0.0.1:8000/group/2/ -H 'Authorization: Token 38a7db568458feb9f8d6008805bba3fca7e1ca00' {"detail":"You do not have permission to perform this action."}

期待通りですね。

info
  • View クラスに get_ メソッドを定義することで動的に機能を設定できると前述しました。
  • 例えば HTTPメソッドごとに 設定するパーミッションを切り替えたいときは get_permissions を使って次のように書けます。
  • def get_permissions(self): self.permission_classes = { 'GET': [IsAuthenticated], 'POST': [IsAuthenticated], 'PATCH': [IsAuthenticated, IsAdmin], 'DELETE': [IsAuthenticated, IsAdmin], }.get(self.request.method, []) return super(MyAPIView, self).get_permissions()
  • これは APIView, GenericView, Viewsets で使えます。
  • 関数型のビューは実践で使ったことないのでわかりません。(誰か知ってたら教えて)
リファレンス

Filtering

APIを作成するときに検索機能は欠かせません。

単純なフィルタリングであれば get_queryset メソッド から フィルタリングしたクエリセットを返せばいいのですが、検索とかを自力で実装するのは結構だるいものです。

そこで Generic Filtering を使います。

フィルタは filter_backends 属性に 定義することで 利用可能になります。

ここでは 3つのバックエンドを紹介します。

DjangoFilterBackend
  • DjangoFilterBackend はフィールドに対応する完全一致フィルタのようです。

  • 対象のフィールドを filter_fields に複数指定します。

  • info
    • django-filter というライブラリが必要です 最初に インストールはお好みでと言いましたが、このフィルタリングをしたい方はインストールしてください。

    • 設定は簡単で settings に対して以下を追記するだけです

      • INSTALLED_APPSdjango_filters を追加
      • DEFAULT_FILTER_BACKENDS('django_filters.rest_framework.DjangoFilterBackend',) を追加
      INSTALLED_APPS = ( # : # : 'django_filters', ) REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',) }
SearchFilter
  • View の search_fields 属性にフィールド名を指定するだけで利用できます。 一致ルールはデフォルトで部分一致ですがプリフィックスを指定することで変更できます。

    ^
    • 前方一致
    =
    • 完全一致
    @
    • 全文検索(MySQLのみサポート)
    $
    • 正規表現一致
  • デフォルトでは search パラメータが使われます。

  • 変更する場合は settings の SEARCH_PARAM を変更します。 簡単ですね。

  • REST_FRAMEWORK = { 'SEARCH_PARAM': 'q', }
OrderingFilter
  • フィルタと言うと情報を制限するイメージがあるんですが、 DRFではソート機能はフィルタという概念として提供されるようです。
  • ordering パラメータ にフィールド名を指定することで そのフィールドでソートできます。 フィールド名だけを指定した場合は 昇順 となり、 先頭に - を加えることで 降順 になります。
  • この機能を有効化するためには ordering_fields 属性に ソートを許可するフィールド名を タプルで 指定します。 全フィールド許可の場合は __all__文字列で 指定します。
  • デフォルトのソートを指定するには ordering 属性にフィールドをタプルで指定します。

実際に以下のいずれかのルールにマッチし、IDかユーザ名でソートするようなビューを定義してみます。

  • username に部分一致する
  • email に前方一致する
class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ('id', 'username',) class UserListView(generics.ListAPIView): #pagination_class = MyPagination serializer_class = UserSerializer queryset = User.objects.filter() filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter) filter_fields = ('username',) search_fields = ('username', '^email') ordering_fields = ('id', 'username') ordering = ('id', 'username',)
# フィルタ条件なし(全県 $ curl -X GET 'http://127.0.0.1:8000/user/' [{"id":1,"username":"test1"},{"id":2,"username":"test2"},{"id":3,"username":"test3"}] # username が test1 と完全一致するレコード $ curl -X GET 'http://127.0.0.1:8000/user/?username=test1' [{"id":1,"username":"test1"}] # 完全一致でないと抽出できない $ curl -X GET 'http://127.0.0.1:8000/user/?username=1' [] # username か email(前方) に 3 を含む user $ curl -X GET 'http://127.0.0.1:8000/user/?q=3' [{"id":3,"username":"test3"}] # id フィールドの昇順 $ curl -X GET 'http://127.0.0.1:8000/user/?ordering=id' [{"id":1,"username":"test1"},{"id":2,"username":"test2"},{"id":3,"username":"test3"}] # id フィールドの降順 $ curl -X GET 'http://127.0.0.1:8000/user/?ordering=-id' [{"id":3,"username":"test3"},{"id":2,"username":"test2"},{"id":1,"username":"test1"}]

通常は search パラメータで絞り込まれますが、 今回は settings で q に変えているので q パラメータで絞込文字列を指定します。

リファレンス
error
  • 'RenameAttributes' object is not iterable が発生した場合 DEFAULT_FILTER_BACKENDS がタプルで指定されているかどうかを確認してください。
  • 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',) の最後のカンマが抜けてたりすると発生します。

Pagination

何らかの一覧ページを作成する場合、ページング処理は欠かせません。

特にこだわりがない場合、リファレンスの例に書いてあるように settings に設定を書くだけで終わりです。

REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 100 }

上記のように設定すると 100件ごと にページ切り替えがおこり、 page=5 のようにURLパラメータを指定すると 401 ~ 500 件目のレコードが対象になります (ページ番号は 1 始まりです

settings の設定値はデフォルトとなるため上書きしない限り全ページに適用されます。

これだけでも割りと十分ですが、画面によってページサイズやパラメータを変えたかったりすると、 ページネーションのカスタマイズが必要になってきます。

カスタマイズするためには ページネーションクラス を継承した クラスを定義して、 pagination_class に指定します。 これにも幾つかの種類があります。

PageNumberPagination

ページ番号によってページネーションをする最も一般的なクラスです。上記の例もこれですね。

操作できるパラメータは以下です。

page_size
  • 1ページあたりに表示にするレコード件数です。
page_query_param
  • ページ番号を指定するパラメータ名を指定します。 デフォルトでは page パラメータが使われます。
page_size_query_param
  • ページサイズ(1ページあたりに表示するレコード件数)を指定するパラメータ名を指定します。 デフォルトでは None なので指定はできません。
max_page_size
  • page_size_query_param で指定されたパラメータで設定できる最大ページサイズです。 当然ですが page_size_query_param が 設定されていなければ意味はありません。
last_page_strings
  • 最終ページを示す文字列です。ここには タプルで複数の値が指定でき、 デフォルトは ('last',) となっています。
template
  • これは Browsable API の表示される右上のボタンを描画するためのテンプレートです。
  • レイアウトに不満がないなら多分いじることはないと思います。
  • デフォルトでは rest_framework/pagination/numbers.html が使われます
django_paginator_class
  • ページネータクラスを指定します。 デフォルトでは django.core.paginator.Paginator が使われます。

レスポンスのページネーションは以下の情報を含みます。

count
  • 検索結果に該当した件数です。ページ番号は関係ありません。
next
  • 次の検索結果を表示するための URL を返却します。ない場合は null (None) です。
previous
  • 前の検索結果を表示するための URL を返却します。ない場合は null (None) です。
results
  • 該当レコードを返却します。

ページサイズが 1 の ページネーションを設定してみます。

class MyPagination(pagination.PageNumberPagination): page_size = 1 class UserListView(generics.ListAPIView): pagination_class = MyPagination serializer_class = UserSerializer queryset = User.objects.filter()
# 1ページ目 $ curl -X GET 'http://127.0.0.1:8000/user/' {"count":3,"next":"http://127.0.0.1:8000/user/?page=2","previous":null,"results":[{"id":1,"username":"test1"}]} # 2ページ目 $ curl -X GET 'http://127.0.0.1:8000/user/?page=2' {"count":3,"next":"http://127.0.0.1:8000/user/?page=3","previous":"http://127.0.0.1:8000/user/","results":[{"id":2,"username":"test2"}]} # 3ページ目 $ curl -X GET 'http://127.0.0.1:8000/user/?page=3' {"count":3,"next":null,"previous":"http://127.0.0.1:8000/user/?page=2","results":[{"id":3,"username":"test3"}]} # 4ページ目(存在せず) $ curl -X GET 'http://127.0.0.1:8000/user/?page=4' {"detail":"Invalid page."}

LimitOffsetPagination

レコードを切り出す位置を指定するページングするクラスです。 DBとの limit, offset にそのまま渡して利用できるので柔軟なページングが実現できます。

default_limit
  • limit が省略された場合に使われるデフォルト値です。
limit_query_param
  • limit (何件抽出するか)を指定するためのパラメータを指定します。デフォルトでは limit パラメータが使われます。
offset_query_param
  • offset (何件目から切り出すか)を指定するためのパラメータを指定します。デフォルトでは offset パラメータが使われます。
max_limit
  • limit によって抽出できる最大件数を指定します。デフォルトでは None で制限なしです(!)
template
  • PageNumberPagination と同じです。

レスポンスの形式は PageNumberPagination と同じです。

CursorPagination

これは 前後 へのページ移動だけを許可するクラスです。 上で紹介した2つのクラスと違い、特定のページ番号に直接アクセスする手段を与えません。

ということなんですが、実はBase64エンコードしたパラメータを cursor に与えることで表示位置を特定しているようです。

例えば /api/users/?cursor=cj0xJnA9Mw%3D%3D というURLであれば、 r=1&p=3 のというパラメータが指定されます。 encode_cursor のソースコード を見る限りでは以下のようなパラメータになっているようです。 (意味については詳しく調べてません

r
  • reverse
p
  • position
o
  • offset

ちなみに、このページネーションはデフォルトで created フィールドでソートするため、 Userのように存在しないモデルに対してこのページネーションを使うと以下のようなエラーになります。

error
  • django.core.exceptions.FieldError: Cannot resolve keyword 'created' into field.

なので、大抵は次のように継承し、別のソート条件を指定して使うことになると思います。

class MyCursorPagination(CursorPagination): ordering = ('id', ) class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer pagination_class = MyCursorPagination

ordering の指定によりレコードの順番が完全に保証できる場合、 レコードが途中で増えたとしても検索結果がずれて同じレコードが表示されるということが発生しません。

そのため、頻繁に追加されるレコードをリアルタイムで一覧するようなページに適しています。

これが CursorPagination のメリットだそうです。

info
  • 通常、ページングというのは SQLでは LIMIT 句と OFFSET 句によって実現されますが、 DBの内部では OFFSET で指定された行数より前のレコードも取り出されているので、 遡れば遡るほど OFFSET の数値が大きくなりページングは重くなっていきます。
  • IDのような一意かつ大小関係を持つ列をもとにWHERE句で絞ってから 表示するというのが1つの解決策のようで、 CursorPagination はまさにそれの実装ですね。

BasePagination

カスタマイズ用のページネーションクラスです。

この処理は 上記のクラスとは違い実際に処理を書く必要が出てきます。

具体的には paginate_queryset(self, queryset, request, view=None)get_paginated_response(self, data) を上書きします。

カスタマイズした ページネーションクラス は pagination_class クラス変数 として View クラス に指定します。ここは解説しません。

リファレンス

Parser

リクエストパラメータの解析方法を指定します。

例えば、 JSON だけを許可する場合は settings のキーに DEFAULT_PARSER_CLASSESJSONParser のモジュールパスを指定します。

REST_FRAMEWORK = { 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', ) }

ビューによって変更したい場合は parser_classes 属性にタプルで指定します。

from rest_framework.parsers import JSONParser, FormParser class EchoView(APIView): parser_classes = (JSONParser, FormParser) def post(self, request): return Response(request.data) urlpatterns += [url(r'^echo/$', EchoView.as_view())]

リクエストを Content-Type にあった 形式で投げてみます

# form-data 形式のパラメータ $ curl -X POST 'http://127.0.0.1:8000/echo/' -d 'a=1&b=2' {"a":"1","b":"2"} # json 形式のパラメータ $ curl -X POST 'http://127.0.0.1:8000/echo/' -H 'Content-Type: application/json' -d '{"a":1,"b":2}' {"a":1,"b":2} # content-type とデータの形式が合わないとエラー $ curl -X POST 'http://127.0.0.1:8000/echo/' -H 'Content-Type: application/json' -d 'a=1&b=2' {"detail":"JSON parse error - Expecting value: line 1 column 1 (char 0)"}

ちなみに parser_classes を JSONParser だけにすると ..

$ curl -X POST 'http://127.0.0.1:8000/echo/' -d 'a=1&b=2' {"detail":"Unsupported media type \"application/x-www-form-urlencoded\" in request."}

DRFに用意されているのは以下で、 Content-Type に指定された MIME-Type によって どれを使うか決定します。

パーサ
    • 対象のMIMEタイプ
    • 説明
JSONParser
    • application/json
    • JSON を解析するためのパーサ
FormParser
    • application/x-www-form-urlencoded
    • URLエンコード形式のパラメータを解析するためのパーサ。 例) a=1&b=2
MultiPartParser
    • multipart/form-data
    • マルチパート形式の パラメータを解析するためのパーサ。ファイルアップロードとかに使われる。
FileUploadParser
    • */*
    • 上記のどれにも該当しないデータの受取に使われるパーサ。受け取ったデータは request.data['file'] に入る。

ドキュメントの記述は見つけられませんでしたが、指定しない場合でも JSONParser, FormParser はデフォルトで使えるみたいです。

リファレンス

お疲れ様でした。よいDRFライフを。