この投稿は 「Djangoのカレンダー | Advent Calendar 2022 - Qiita」 1日目の記事です。
この記事では、11月に「みんなのPython勉強会#87」で発表したトーク 「Django REST Framework はじめの一歩 〜押さえておきたい3つのポイント〜」の内容を詳しく説明したいと思います。トークの目的は「Django REST Framework(通称 DRF)がどんなものかをざっくり理解する」です。 DRF はよく分からないけど、Django なら何となく分かるという DRF 初心者の方を理想のトーク対象者として想定しています(もちろん Django を知らなくてもウェルカム!)。
ちなみに、各種バージョンは
- Django : 3.2 LTS(現時点での最新 LTS)
- DRF : 3.14(現時点での最新バージョン)
で動作確認をしています。
はじめに
今回のトークに先立って事前に Twitter でアンケートを取ったところ、約73%の方が「DRF を使っている」と回答していた のですが、私の予想を遥かに超えて DRF が使われていることに驚きました(正直半々くらいかなと思っていました…)。
【Django アンケート】
— akiyoko / 現場で使えるDjangoの教科書《基礎編》(3.2対応版)@技術書典11 (@aki_yok) October 25, 2022
DRF(Django REST Framework)を使っていますか?
(※11月10日の「みんなのPython勉強会#87」で発表予定のトーク『Django REST Framework はじめの一歩(仮)』用のアンケートです。)#Django #stapy
ここで、アンケートにご協力いただいた皆さまにこの場を借りて感謝申し上げたいと思います。ご協力ありがとうございました。
また、「Django Developers Community Survey 2021」という7000名を超える開発者アンケートでは、「好きな Django パッケージは何ですか?(複数回答ありで5つまで回答可)」という質問に対して、DRF が第1位に選出されていました。2位の「django-celery」と比較してもダブルスコアというぶっちぎりの結果になっていますね。
DRF とは?
本題に入る前にまず、DRF とは何か?を説明したいと思います。一言でいえば、DRF は「REST API を作成する際の多彩なニーズに応えるための超ド定番 Django パッケージ」です。 Django 単体では不足しているさまざまな機能を、DRF で補完してくれているというイメージになります。特に、Django 開発者なら理解しやすい作りになっているというのが、Django 開発者にとってうれしい特徴です。
続いて、REST API の概要についても説明します。
REST API はスマホや SPA のバックエンドとして よく利用されています(左図)。そして、REST API は、URI でリソースを特定し、HTTP メソッドでそのリソースへの操作を表すという直感的に理解しやすいシンプルな設計になっています(右図)。そんな共通の枠組みに則ったデータを介すことで、フロントエンドとバックエンドが疎結合化、すなわち、お互いのプログラミング言語やアーキテクチャを意識することなくデータをやり取りすることができるのです。なお、REST API はリソース中心の設計となっているので、モデル中心の Django と親和性が高いというのも DRF を使うメリットになるかと思います。
DRF が一番得意なのは、単一リソースの CRUD を処理する REST API です。上の表に示したように、単一リソースの CRUD を処理する REST API は、「GET」「POST」「PUT」「PATCH」「DELETE」という HTTPメソッドそれぞれに意味を持たせ、リソースを表す URI と組み合わせて、「リソース一覧の取得」「リソース詳細の取得」「リソースの登録」「リソースの更新」「リソースの一部更新」「リソースの削除」といった API を構成します。これを DRF で実装するのはすごく簡単で、モデル除けば最短20行くらいで書くことができます(コード例については後ほど紹介します)。
ここで、DRF についてよくある「誤解」を紹介します。
よくある誤解その①「DRF では単一リソースを扱う API 以外は作れない」は「✕」です。先に説明した 単一リソースを扱う REST API 以外にも、上の表に示したような、リソースがネストした API、独自アクションを追加した API、リソースに紐付いていない API なども DRF で作成することができます。
よくある誤解その②「DRF なしでは JSON のやり取りができない」も「✕」です。Django 単体でも(やろうと思えば)JSON データを受け取って JSON データを返すことができます。下の例を見てください。
api/views.py
import json from django.forms.models import model_to_dict from django.http.response import JsonResponse from django.views import View from shop.models import Book class BookCreateView(View): """本モデルの登録APIクラス""" def post(self, request, *args, **kwargs): """本モデルの登録APIに対応するハンドラメソッド""" data = json.loads(request.body) # バリデーションを実行 if not data.get('title'): return JsonResponse({'message': 'タイトルは必須です。'}, status=400) # モデルオブジェクトを登録 book = Book(**data) book.save() # レスポンスオブジェクトを作成して返す return JsonResponse(model_to_dict(book), status=201)
入力パラメータとなる JSON データは request.body に入っているのでそれをパースして、(この例ではめちゃくちゃ適当ですが)バリデーションを適宜実行して、モデルオブジェクトにデータを詰め替えて永続化して、最後に JsonResponse オブジェクトを作成して返しています。でもこんなことを毎回するのは大変ですよね。できれば Django っぽい書き方で、こういった処理を共通化したいというニーズから DRF が誕生したんじゃないかなと思います。
次は、DRF を使うメリットについて。
まず、Django のノウハウやエコシステムが活用できるというのが、DRF を使う大きなメリットになります。 むしろこれがほとんどすべてと言っても過言ではありません。DRF プロジェクトのベースはあくまでも Django なので、ORM やマイグレーションを始めとする多くの機能をフル活用できたり、いつも使っている Django パッケージが使えたりといったように、Django のノウハウやエコシステムをほぼそのまま使うことができます。
そして次に、人気があって枯れていることもメリットです。DRF は Django で REST API を作るとなった場合にほぼ一択になるほど人気で、特に英語圏での情報がたくさんあります。 公式ドキュメントやチュートリアルも(英語になりますが)かなり充実しています。
機能が充実していることも DRF ならではのメリットです。 例えば、Cookie認証、トークン認証、JWT認証などの API でよく利用される認証を簡単に利用する ことができます。また、アクセス権制御をするための「パーミッション」機能、クエリ文字列による検索をするための「フィルタリング」機能、「ページネーション」機能、APIの利用回数制限をするための「スロットリング」機能などが用意されており、それぞれが差し替えできるようになっています。さらに、画面で API を実行確認できる「Browsable API」と呼ばれるテストクライアントがすぐに使えたり、OpenAPI に準拠した API ドキュメントを出力 したりすることもできます。こういった REST API を作成するのに必要な機能がたいてい揃っていて、簡単な設定をするだけで幅広いニーズを満たすことができるというのが大きな特徴になります。
逆に、DRF にはどんなデメリットがあるのでしょうか。
ズバリ… 複雑です。
Django だけでも十分複雑なのに、そこに DRF が加わったらさらに複雑になってしまいますよね。
そこでここからは、DRF 初心者が押さえておきたい次の3つのポイントを解説していきたいと思います。
- ポイント①:全体像を把握しよう
- ポイント②:シリアライザはフォームっぽく使える
- ポイント③:起点は DRF 用ビュー
この3つについてそれぞれ詳しく説明していきます。
ポイント①:全体像を把握しよう
ポイント①「全体像を把握しよう」について。
まず比較のために、Django プロジェクトの全体像と登場人物について説明します。図の左側から来たリクエストを、URLディスパッチャと呼ばれる Django のコアコンポーネントがハンドリングし、URLconf で登録したビュー関数を呼び出して、ビューの中でフォームやモデル、テンプレートを使って、レスポンスを作成して返し、URLディスパッチャがレスポンスを処理するという流れになっています。ミドルウェアは、ビューが呼び出される前や後で何らかの処理をするために利用されます。
REST API を構築するための DRF プロジェクトでは、(HTML のフォーム要素を扱わないし、レスポンスとして画面をレンダリングした HTML を返さないので)基本的にフォームとテンプレートは使いません(後述しますが、Django を共存させる場合にはその限りではありません)。その代わり、DRF では「シリアライザ」というコンポーネントを使います。 シリアライザは入出力の JSON(およびモデルとの連携)定義とバリデーションを担当します。ポイント②で詳しく説明しますが、シリアライザはフォームに似せて作られていて、フォームと同じように使うことができます。
そしてビューは、DRF用のビューを使います。これもポイント③で説明しますが、図を見ると分かるように、DRF の起点となるのは「DRF 用ビュー」です。図では「✕」で示していますが、通常の Django のビューを URLconf に登録しておくことも可能です。
それ以外のモデル、URLconf、ミドルウェアは通常の Django と同じように使います。
ポイント①のまとめです。DRF では、DRF用のビュー を作成して URLconf に登録します。そしてシリアライザというコンポーネントを使い、フォームとテンプレートは使いません。ただし、Django と DRF のビューをそれぞれ登録することができるので、そういった共存をさせる場合にはフォームもテンプレートも使うことになるので誤解のないようにしてください。それ以外は通常の Django とだいたい同じように使います。こういった全体像を押さえておくのが第一のポイントです。
ポイント②:シリアライザはフォームっぽく使える
ポイントその②「シリアライザはフォームっぽく使える」について。
その前にまず、DRF の独自コンポーネントである「シリアライザ」の使いどころを説明します。シリアライザが担当する役目は、
- 入力データのバリデーション
- モデルオブジェクトの永続化
- 出力データの作成
です。上の図はどこでシリアライザを使うかを示しているのですが、登録API、更新API、取得API のそれぞれでシリアライザの使いどころが異なります(削除API のようにシリアライザを使わない場合もあります)。例えば登録 API だと、リクエストされた JSON データを元にしてシリアライザオブジェクトを作成し、シリアライザオブジェクトのバリデーションメソッドを実行し、シリアライザの永続化メソッドを実行して、最後にレスポンスオブジェクトの引数にシリアライザのデータを渡すという流れになります。このように、ほとんどの場合でシリアライザを使うことになるため、DRF を使うときにはシリアライザは避けて通ることができません。
さて、本題です。シリアライザはフォームっぽく使えるということについて説明します。リソース(すなわちモデル)と紐付いた REST API の場合は「ModelSerializer」を使うのが基本なのですが、ModelSerializer の使い方は(フォームで特定のモデルを扱うための)ModelForm とほぼ同じです。 ModelSerializer と ModelForm の引数や属性、バリデーション、永続化の流れを上の図に並べて示したのですが、仕組みがほぼ同じになっています。バリデーションメソッドは「is_valid」、永続化メソッドは「save」でこちらも同じです。ただし、バリデーション済みのデータは「validated_data」となっていて、ModelForm(親クラスの Form も同様)と変数名が異なるので注意してください。
シリアライザの実装イメージは次のようになります。
api/serializers.py
from rest_framework import serializers from shop.models import Book class BookSerializer(serializers.ModelSerializer): """本モデル用シリアライザ""" class Meta: model = Book fields = ['id', 'title', 'price']
ModelSerializer を継承したシリアライザクラスを用意して、Metaクラスの「model」属性に連携させるモデルクラス、「fields」属性に JSONの入出力フィールドとして利用するモデルのフィールド名を指定します。フィールドは、fields 属性以外に、除外するフィールドを excludes 属性で指定することもできます。
使い方は ModelFormとほぼ同じで、引数「data」にリクエストのパラメータ(DRF では JSON データ)、更新時には引数「instance」にモデルオブジェクトを指定します。バリデーションをおこなたいときは is_valid()、モデルオブジェクトを永続化したいときは save() メソッドを実行します。
>>> from api.serializers import BookSerializer >>> serializer = BookSerializer(data={'title': 'DRFの本', 'price': 1000}) >>> serializer.is_valid() True >>> serializer.validated_data OrderedDict([('title', 'DRFの本'), ('price', 1000)]) >>> book = serializer.save() >>> serializer = BookSerializer(instance=book, data={'price': 2000}, partial=True) >>> serializer.is_valid() True >>> serializer.save() <Book: DRFの本>
ポイント②のまとめです。モデルと紐付いた REST API の場合は ModelSerializer を使うのが簡単で、その使い方は ModelForm とほぼ同じです。
ポイント③:起点は DRF 用ビュー
最後のポイント「起点は DRF 用ビュー」について。
DRF には「rest_framework.views.APIView」というクラスが用意されているのですが、「APIView」は DRF の起点となる基底ビュークラスで、これを継承することで、さまざまな前処理と後処理をやってくれます。
1. リクエストオブジェクトを DRF 用に変換
2. レンダラクラスとメディアタイプを決定
3. バージョニング
4. 認証チェック
5. パーミッションチェック
6. スロットリング
7. 例外ハンドリング
これらの処理は差し替え可能です。2. 〜 6. には代表的な処理クラスがいくつか用意されていて、デフォルトの設定を設定ファイル(settings.py)の「REST_FRAMEWORK」で上書きして全体的な設定をしたり、クラス変数をオーバーライドしてビュークラス単位での個別設定をしたりすることができます。
DRF 用のビューは大きく分けて3種類あり、用途に応じて
1)rest_framework.views.APIView
2)rest_framework.generics.CreateAPIView などの 汎用 APIView
3)rest_framework.viewsets.ModelViewSet 系ビュー
のいずれかを継承して作成します。 どれを選択するかによってそれぞれカスタム性やコード量が異なってきますが、この後で具体的な実装を見ながら説明していきます(コードがたくさん出てきますが、雰囲気を理解してもらえれば大丈夫です)。
ちなみに下の図で示すように、2) と 3) のビューは「APIView」の子クラスで、APIView は「基本汎用ビュー」と呼ばれる Django の「View」クラスの子クラスです。
まず、1)の APIView を継承したビューの書き方について説明します。
例えば、更新 API で呼び出される post メソッドでは、モデルオブジェクトと入力データからシリアライザオブジェクトを作成し、is_valid() で入力データのバリデーションをおこない、save() でモデルオブジェクトを更新して、最後にシリアライザの data 属性の値を使ってレスポンスオブジェクトを作成して返しています。
api/views.py
from rest_framework import status, views from rest_framework.generics import get_object_or_404 from rest_framework.response import Response from shop.models import Book from .serializers import BookSerializer class BookRetrieveUpdateAPIView(views.APIView): """本モデルの取得(詳細)・更新APIクラス""" def get(self, request, pk, *args, **kwargs): """本モデルの取得(詳細)APIに対応するハンドラメソッド""" # モデルオブジェクトを取得 book = get_object_or_404(Book, pk=pk) # シリアライザオブジェクトを作成 serializer = BookSerializer(instance=book) return Response(serializer.data, status.HTTP_200_OK) def put(self, request, pk, *args, **kwargs): """本モデルの更新APIに対応するハンドラメソッド""" # モデルオブジェクトを取得 book = get_object_or_404(Book, pk=pk) # シリアライザオブジェクトを作成 serializer = BookSerializer(instance=book, data=request.data) # バリデーションを実行 serializer.is_valid(raise_exception=True) # モデルオブジェクトを更新 serializer.save() # レスポンスオブジェクトを作成して返す return Response(serializer.data, status.HTTP_200_OK)
コードは長くなりますが、好きなように書くことができます。しかし、単一リソースの CRUD 処理をする場合、モデルとシリアライザだけが違うビューを追加する場合に同じようなコードを何度も書くのはさすがに冗長ですよね。
そんな場合に利用したいのが、2)の「汎用 APIView」です。上の表に示したように、対応アクションごとにいろいろな汎用 APIView が用意されているので、用途に合わせてどれかを継承して、モデルオブジェクトを取得するためのクエリを指定するための「queryset」とシリアライザクラスを指定するための「serializer_class」をクラス変数を加えるだけです。
これでだいぶ短く書くことができますが、単一モデルのCRUD処理をするというのを逸脱した API を作ろうとすると、親クラスのメソッドをオーバーライドして拡張する必要があります。
api/views.py
from rest_framework import generics from shop.models import Book from .serializers import BookSerializer class BookListCreateAPIView(generics.ListCreateAPIView): """本モデルの取得(一覧)・登録APIクラス""" queryset = Book.objects.all() serializer_class = BookSerializer class BookRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView): """本モデルの取得(詳細)・更新・一部更新・削除APIクラス""" queryset = Book.objects.all() serializer_class = BookSerializer
config/urls.py
from django.urls import path from api import views urlpatterns = [ path('api/books/', views.BookListCreateAPIView.as_view()), path('api/books/<pk>/', views.BookRetrieveUpdateDestroyAPIView.as_view()), ]
最後に、3)ModelViewSet 系ビューを継承したビューの書き方です。冒頭で「単一リソースの CRUD を処理する REST API なら超簡単」で「20行くらいで API が全部書ける」と言っていたのはこちらです。
上の表に書いたように ModelViewSet は「一覧」「詳細」「登録」「更新」「一部更新」「削除」という6つの API を全部網羅しています。 実装は、下に示したように ModelViewSet を継承して、「serializer_class」と「queryset」の指定をするだけです。
api/views.py
from rest_framework import viewsets from shop.models import Book from .serializers import BookSerializer class BookViewSet(viewsets.ModelViewSet): """本モデルのCRUD用APIクラス""" serializer_class = BookSerializer queryset = Book.objects.all()
あとは、ViewSet 用の SimpleRouter(または DefaultRouter)を利用して URL パターンを URLconf に追加してあげれば、これで API の実装は完了です。シリアライザを含めて最短20行ほどで書くことができます。
config/urls.py
from django.urls import include, path from rest_framework import routers from api import views router = routers.DefaultRouter() router.register('books', api.BookViewSet) urlpatterns = [ # すべてのアクション(一覧・詳細・登録・更新・一部更新・削除)をまとめて追加 path('api/', include(router.urls)), ]
なので、単一モデルの CRUD を処理する REST API をまるっと実装したいというニーズには「ModelViewSet」が一番最速で実装できる ということになります。
ポイント③のまとめです。単一モデルの CRUD を処理する REST API をまるっと実装したい場合は ModelViewSet 系ビューを使うのが一番簡単で、複雑なことがしたい場合は APIView(または GenericAPIView)を継承したビュークラスを使うのがよいでしょう。
まとめ
最後のまとめです。
DRFを使う場合、Django に DRF の要素、コンポーネントが加わることになるので複雑になります。なのでまずは、全体像を把握しましょう。一見複雑にみえますが、新しい要素は「シリアライザ」と「DRF用ビュー」です。
「シリアライザ」は入力データのバリデーション、内部に保持したモデルオブジェクトの永続化、出力データの作成を担当するクラスで、Django のフォームと同じような使い方ができます。
「DRF用ビュー」は大きく3種類あり、まるっと API 全部を実装できてしまうものもあれば、複雑なカスタム API にも対応できるものもあるので、用途に合わせて親クラスと書き方を使い分けてください。
さてこれで、DRF がどんなものか完全に理解できたでしょうか。もっと DRF のことを深く知りたいという方は、「現場で使える Django REST Framework の教科書」という 220ページ以上まるまる DRF について書かれた素敵な本があるので、そちらを読んでみてください。ここで説明した内容をもっとディープに解説しています。そして何と、今月この本が改訂予定です。Django 3.2 LTS、DRF 3.14 対応で、Vue 3(+ Vite)と Vuetify 3 でチュートリアルを刷新しています。
トークのスライドはこちらにアップしています。
宣伝
Django の技術同人誌をこれまでに4冊出しました。開発のお供にどうぞ。
現場で使える Django の教科書《基礎編》
「現場で使える Django の教科書」シリーズ第1弾となる Django の技術同人誌。Django を現場で使うための基礎知識やベストプラクティスについて、初心者・初級者向けに解説した本です。B5・本文192ページ。Django 3.2 LTS に対応。
★ Amazon(電子版/ペーパーバック)
★ BOOTH(紙の本)
現場で使える Django の教科書《実践編》
《基礎編》の続編にあたる「現場で使える Django の教科書」シリーズの第2弾。認証まわり、ファイルアップロード、ユニットテスト、デプロイ、セキュリティ、高速化など、さらに実践的な内容に踏み込んでいます。現場で Django を本格的に活用したい、あるいはすでに活用している方にピッタリの一冊。B5・本文180ページ。
★ Amazon(電子版/ペーパーバック)
★ BOOTH(紙の本)※在庫なし
現場で使える Django REST Framework の教科書
Django で REST API を構築する際の鉄板ライブラリである「Django REST Framework」(通称「DRF」)にフォーカスした、「現場で使える Django の教科書」シリーズの第3弾。B5・本文228ページ。Django 3.2 LTS に対応。
★ Amazon(電子版/ペーパーバック)
★ BOOTH(紙の本)※在庫なし
現場で使える Django 管理サイトのつくり方
Django の管理サイト(Django Admin)だけに特化した、ニッチでオンリーワンな一冊。管理サイトをカスタマイズする前に絶対に読んでほしい本です。B5・本文152ページ。
★ Amazon(電子版)
★ BOOTH(紙の本)