Django Rest Frameworkの便利さを理解する


はじめに

この記事は前回の続きです。前回の記事はAPIの概念的な部分を説明しています。今回は実際にPython, Djangoを用いてAPIを構築していきたいと思います。今回作るAPIはデータを(ブラウザ上から)送るとデータベースに保存し、任意のエンドポイントに移動するとデータベース内のデータを表示するというシンプルなものとしました。またDjango Rest Frameworkを使わずにAPIを構築します。理由としては、あえてDjangoの用意するフレームワークを使わずにPythonコードで構築することで、Django Rest frameworkの利便性が理解できると思った為です。

対象者

Pythonの基本的な文法は知っている方、Djangoの基本的な仕組みを知っている方。(Djangoのインストール、導入部分はかなり省いています。)

初期設定

APIを実際に作っていく上でまず重要になるのは、どのようなデータを扱うのかの部分です。APIに向けてデータを送ればデータベースに勝手に保存されるという訳ではなく、APIはmodels.pyで設計されたデータのモデルを元にデータを保存します。なのでまずはどのようなデータを扱うのかの部分をモデルに設定します。

models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=100)
    description = models.TextField()
    timestamp = models.DateField(auto_now_add=True)

    def __str__(self):
        return self.title

今回はブログのデータをAPIで扱うことを仮定したモデルを設計しました。タイトルがあり、記事の内容、投稿日時があるデータです。モデルが書けたらmigrateまでやっておきましょう。次にadmin.pyPostを登録し、adminページからテストデータを幾つか入れます。下の図のように適当な値を項目ごとに入れて保存します。

任意のエンドポイントでデータを表示させる

それではadminページから保存したデータを任意のエンドポイント上に表示させてみしょう。まず思い出す必要があるのが、最近のAPIはリクエストを送るとJSONデータが返ってくることです。なのでviews.pyのposts関数の返り値はJsonResponseになっています。それ以外はシンプルなコードでQuerySetをJSONデータに近付けています。QuerySetをfor文で回して辞書型にして、それをリストに入れ込んでいきます。ここからはDjangoが自動でやってくれる仕事で、Pythonのリストに入った辞書型のデータはJSONデータに自動でシリアル化してくれます。

views.py
from .models import Post

def posts(request):

    posts = Post.objects.all()
    post_list = []
    for post in posts:
        post_dict = {
            'pk': post.pk,
            'title': post.title,
            'description': post.description,
            'timestamp': post.timestamp,
        }
        post_list.append(post_dict)

    return JsonResponse(post_list, safe=False)

    # non-dict型のデータをjsonデータに変換するには"safe=False"を記述する。

任意のエンドポイントを作成。

urls.py
from application_name.views import posts #追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/post/', posts), #追加
]

ここまで来たら、一回ローカルサーバーを立てて作成したエンドポイントに移動してみましょう。今回はプライマルキーを入れる必要はありませんでしたが、このようにテスト時に確認する際は便利ですね。ちゃんとデータの確認ができたので成功ですね!次はPOSTリクエストについてです。

POSTリクエストに対する処理を書く

Djangoを用いたAPIの役割として、models.pyに基づいた新しいデータがPOSTリクエストを通して送られてきた場合、そのデータをデータベースに保存するというものがあります。少し書いていても分かりづらいので簡潔に説明しますと、API側の役割は送られてきたデータを保存しやすい形に加工して保存することです。そこで次は加工して、保存するコードを書いていきます。

views.py
#以下のコードを追加します。if文以下はさっきのGETリクエストの処理の上に書きます。
from django.http import HttpResponse
import json

def posts(request):

    if request.method == 'POST':
        data = json.loads(request.body)
        post = Post(
            title=data['title'],
            description=data['description'],
            )
        post.save()
        return HttpResponse(201)

    posts = Post.objects.all()
    # ~~ 以下略 ~~

Djangoは送られてきたJSONデータに対して自動的にデシリアライズ(JSONデータを扱いやすい形に戻すこと)する機能は標準で備わっていないので、デシリアライズするコードを書いておく必要があります。Pythonで扱えるデータ形式に戻さなければデータベースに保存できないので、この機能は必要です。JSONデータを適当な変数に入れ、この変数を用いてデシリアライズ(加工)します。またDjangoはviewsから自動的にデータを保存する機能がありませんので最後にpost.saveと明示的に書きます。

実際にPOSTリクエストを送ってみる

今回はaxiosを用いてAPIに対してPOSTリクエストを送ってみたいと思います。ajaxを自身のアプリケーションにおいて実現させたい場合はJavascriptのaxiosライブラリを使うのがオススメです。まずはPOSTリクエストを送るページをtemplatesに作成します。そのページにcdnを用いてaxiosを追加していきます。

main.html

    <body>
        <p>Test Page</p>   
    </body>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script> #cdnで追加
</html>

viewにfunction basedで作成。ルーティングも行っておく。

views.py
def main_page(request):
    return render(request, 'main.html', context=None)
urls.py
from application_name.views import main_page #追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', main_page),    #ルートを追加する
    path('api/post/', posts),
]

ローカルサーバーを立ててmain.htmlが表示されるURLに移動します。そこでデベロッパーツールのコンソールからAPIがちゃんと動くかどうか確かめてみましょう。POSTリクエストで送信するデータはJSONデータである必要があるため、辞書型にする。ただ引数として分かりやすくするために、変数にしておきます。

console
/* 
    async関数を作る
    requestを送り、responseを作る
    requestを送るには時間がかかるためawaitさせたい
    サーバーが既に立っているのでAPIのエンドポイントは相対パスで良い
*/

async function addPost(title, description) {
    const response = await axios.post('api/post/', {'title': title, 'description': description})
    console.log(response)
}

// インスタンス化
addPost('sending from axios', 'some test messages')

ただこれを実行しても403 Forbiddenのエラー文が帰ってくるはずです。このように403が帰ってくる時は、ブラウザのデベロッパーツールのNetworkで自分が送った全てのリクエストを確認することができます。下の画像の通り、送ろうとしていたペイロードの部分(データ)は確認できます。

次にResponse部分を見てみるとよく見るhtml文が並んでいます。分かりやすくブラウザでこのレスポンスを確認するために、レスポンスのhtml文をコピーして、innerHTMLで書き換えてみましょう。少し言葉では説明しづらいので下のコードを見てください。

console
document.querySelector('body').innerHTML = `レスポンスのhtml文`

これで実行するとDjango側からエラーが来ていると分かるはずです。一応エラー画面を下に貼っておきます。エラー文を見てみるとCSRF verificationが失敗していると分かります。Djangoでformを使っているならcsrf_tokenを使えと書いてありますが、今回はajaxを用いてデータを送信しているので、この方法では解決できません。Djangoにおいてajaxを行うには普段使わない方法を用います。

そもそもCSRF verificationの流れを説明しますと、フロント側が他の場所からデータにアクセスしているのではなく、同じサイトからアクセスしていることをDjango側に示し、それを受け取ったDjangoはフロント側にデータのアクセス許可を出します。これを踏まえてもう一度403 Forbiddenについて考えると、『あなたはデータを扱う権限(許可)がありません』ということを表していると分かります。
ただ先ほど述べたようにformは使わないのでcsrf_tokenは使えません。なので今回はcsrf_tokenをCookieに持たせてjavascriptのfileに貼り付けます。

sample.js
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"

本来は上のコードを、ajaxリクエストを送るjsファイルの一番上に貼り付けることで、axiosを用いたリクエストにデフォルトでcsrf_tokenを持ったCookieを持たせることができます。これにより先ほどのエラーを解決できるはずです。
今回Javascriptを用いてajax通信以外の機能は持たせませんので、任意の名前のjsファイルを作成し、上のコードのみ書いておきます。このファイルを下にあるようにmain.htmlで読み込み(繋ぎこみ)、デベロッパーツールのコンソールでも動くようにします。

main.html

    <body>
        <p>Test Page</p>   
    </body>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script type="text/javascript" src="{% static 'sample.js' %}"></script> #追加
</html>

先ほどのようにコンソールで引数に値を入れてインスタンス化すると、今度はエラーは出ずにステータスコード201が帰ってきています。つまり成功です!

一応'api/post/'のエンドポイントに移動して、ちゃんとPOSTしたデータが表示されているか確認してみます。
プライマリーキーが3のデータが先ほど追加したデータなので、ちゃんとGET, POSTリクエストが動いていることが分かります。

Django Rest Frameworkを使う理由

まずDjango Rest Framework(DRF)を使うとデータの加工や今回は扱っていない認証に関するコードを大幅に少なくしてくれるという理由が一番かと思います。ただ次に挙げられる理由として私は、PUT、PATCH、DELETEなどのHTTP Verbが使えるようになることだと思います。通常、Djangoはこれらを無視するので使いたいと思っても使えません。DRFは他のリクエストも簡単に扱ってくれるので、APIとしての機能を最大限活かせるようになります。他にも沢山理由があると思いますが、とりあえずこの二つが大きな理由と考えます。

最後に

記事の内容はいかがでしたでしょうか。自分の備忘録としての役割もあり、かなり長いものになってしまったので最後まで読んでくれた方はありがとうございました。間違い、誤字などございましたら、お気軽に指摘していただけると助かります。次回は外部のAPIを用いて簡易的なアプリケーションを作成していきたいと思ってます。