Django REST framework

## 1. やりたいこと


ReactとVueに入門したのでバックエンドAPIも覚えたい


## 2. 実現方法


DjangoでAPIが書けるDjango REST frameworkを使うと簡単につくることが出来るようです
https://www.django-rest-framework.org/

DjangoはDBをモデルとして扱うORMを備えていて、モデルから受け取った値をViewを使って表示するという仕組みになっていますが、Django REST frameworkはモデルから受け取った値をシリアライズしてJSONとして渡してやる、あるいはシリアライズ済みのJSONを受け取ってデシリアライズして処理する、という仕組みで動作になっており、どのURLがどんなAPIになるかについてもフレームワークにまとめて処理できるようになっているようです


こちらの記事が分かりやすかったのでコードをお借りしてやってみます
https://qiita.com/kimihiro_n/items/86e0a9e619720e57ecd8


## 3. プロジェクトをつくりモデルを設定する


モデルをつくって書き込めるようにします


### 3-1. 必要なパッケージのインストール


```terminal
pip install django
pip install djangorestframework
pip install django-filter 
```


### 3-2. プロジェクトをつくる


適当なフォルダを作ってプロジェクトとアプリをつくります
プロジェクト名がdjango_rest_framework_test、アプリ名がblogです


```terminal:terminal
django-admin startproject django_rest_framework_test
cd django_rest_framework_test/
python manage.py startapp blog
```


### 3-3. モデルを定義する


通常のDjangoと同様にモデルを定義します
ここではブログのデータをイメージして、ユーザーと記事エントリーのモデルをつくっています


```python:/blog/models.py
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=32)
    mail = models.EmailField()

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

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


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)
```


Userクラスで__repr__と__str__に書いている処理は、管理者画面でEntryの内容を編集する際に設定しないままだと「User Object」表示されてしまうのを見たい形で表示させる為に書いています


### 3-4. settings.pyに登録


追加したアプリとrest_frameworkをsettings.pyに登録します


```python:/django_rest_framework_test/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
    'rest_framework',
]
```


### 3-5. DBをマイグレートする


追加したモデルを追加してmigrateします


```terminal:terminal
python manage.py makemigrations
python manage.py migrate
```


### 3-6. Django管理者ユーザーを設定する


管理者ユーザーを登録します


```terminal:terminal
python manage.py createsuperuser
```


### 3-7. 管理者画面に項目を追加する


モデルが管理者画面に表示されるようにします


```python:django_rest_framework_test/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
```


### 3-8. 動作確認


Djangoを起動します


```terminal:terminal
python manage.py runserver
```


管理者画面でモデルが出来ているか確認します
http://localhost:8000/admin


ここまで普通のDjangoと同じなのでいつも使っている方は普通に書けるところだと思います


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/0c1842ba-08d1-439a-2fa1-6023baa40b07.png)


## 4. APIをつくる


あとはDjango REST APIを使えば以下の3つを追加すればAPIが動きます

Serializer
ViewSet
URL pattern


### 4-1. serializer.py


まずSerializerをつくります
最小構成ではモデルとフィールドを指定するだけで動いてくれます


```python:/blog/serializer.py
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')
```


### 4-2. views.py


ViewSetsをつくります
views.pyからさっき作ったserializerとモデルを呼んできて指定するだけです


```python:/blog/views.py
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
```


### 4-3. urls.py


URL patternをつくります
routersにViewSetを渡せばURL patternを作ってくれるので、これをincludeしてurls.pyで指定してやればAPIが表示できます


```python:/django_rest_framework_test/urls.py
from django.contrib import admin
from django.urls import path
from django.conf.urls import include
from blog.urls import router as blog_router

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(blog_router.urls)),
]
```


```python:/blog/urls.py
from rest_framework import routers
from .views import UserViewSet, EntryViewSet

router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
router.register(r'entries', EntryViewSet)
```


### 4-4. 動作確認する


起動します


```terminal:terminal
python manage.py runserver
```


urls.pyで指定した通り http://localhost/api 以下にAPIが準備されています


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/9d034128-36a9-5601-366e-f66fc1fa1d51.png" width=60%>


それぞれのAPIの取得例を表示できます


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/2bde5a9b-87aa-15ab-6345-3894472a6362.png" width=60%>


### 4-5. APIを使ってみる


#### 4-5-1. 取得する


PythonからhttpでGETリクエストを送ってみます


```python:python
import requests
import json

url = 'http://localhost:8000/'
res = requests.get(url)
print(res)
print(res.status_code)
values = json.loads(res.text)
print(values)
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/fc9b3382-94df-42f8-fc34-a60a299700af.png" width=90%>


ちゃんと取れました


#### 4-5-2. 登録する


PythonからhttpでPOSTリクエストを送ってみます


```python:python
json_data = {"name": "ほが三郎",
             "mail": "xxxx@fugamail.com"}
url = 'http://localhost:8000/users/'
response = requests.post(url, data = json_data)
response
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/9a8d1744-6714-3d64-a988-7ed5ccb6d519.png" width=70%>


追加できました


## 5. リレーションモデル


こちらの記事を参考にやってみました
https://sakataharumi.hatenablog.jp/entry/2018/10/20/010806


### 5-1. 現状


現状でentriesを取得すると、author欄にはuser IDが入って表示されます


```python:python
url = 'http://localhost:8000/entries'
res = requests.get(url)
print(res)
print(res.status_code)
values = json.loads(res.text)
print(values[0])
```


```js:取得されるJSON
{'title': 'てすと１',
 'body': 'ほげほげほげほげ\r\nふがふがふがふが\r\nほげほげほげほげ\r\nふがふがふがふが\r\nほげほげほげほげ\r\nふがふがふがふが',
 'created_at': '2024-02-01T09:12:04.079115Z',
 'status': 'public',
 'author': 1}
```


知りたいのがユーザー名であるような場合、別途ユーザーIDをAPIで取得しなければならずわりとイマイチですので、リレーションモデルを使って記事エントリーを取得する際に著者情報も取れるようにしましょう


### 5-2. リレーションモデルを使う


以下のように取得登録できるようにします


```js:取得するときのJSON
{'title': 'てすと１',
 'body': 'ほげほげほげほげ',
 'created_at': '2024-02-01T09:12:04.079115Z',
 'status': 'public',
 'author': {'id': 1, 'name': 'ほげ太郎', 'mail': 'xxxx@gmail.com'}}
```


```js:登録するときのJSON
{'title': '投稿てすと',
 'body': 'ららららい',
 'status': 'public',
 'author_id': 1}
```


取得時のauthorはUserSerializerから取得した情報をそのまま使いますので、単に author = UserSerializer()と上書きしてやるだけです。これを取得時のみ使いたいのでread_only=Trueにしておきます。一方、登録時はauthor_idをUserのプライマリキーとして受け取るのでPrimaryKeyRelatedFieldで設定してやります。author要素は既に使われているのでクライアント側は名前を変えてauthor_idとして、モデル側はauthorとして返さないといけないのでcreate()の中で置き換えてやります。create()メソッドが受け取る引数は辞書型なので扱いは簡単です


```python:/blog/serializer.py
from rest_framework import serializers
from .models import User, Entry

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'name', 'mail')

class EntrySerializer(serializers.ModelSerializer):
    author = UserSerializer(read_only=True)
    author_id = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), write_only=True)
    class Meta:
        model = Entry
        fields = ('title', 'body', 'created_at', 'status', 'author', 'author_id')

    def create(self, validated_data):
        validated_data['author'] = validated_data.get('author_id', None)

        if validated_data['author'] is None:
            raise serializers.ValidationError("user not found.") 

        del validated_data['author_id']

        return Entry.objects.create(**validated_data)
```


### 5-3. APIを使ってみる


#### 5-3-1. 取得


```python:python
url = 'http://localhost:8000/entries'
res = requests.get(url)
values = json.loads(res.text)
print(values[0])
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/18b50936-b13d-d605-09ef-a7492aa5200f.png)


#### 5-3-2. 登録


```python:python
json_data = {'title': '投稿てすと',
             'body': 'ららららい',
             'status': 'public',
             'author_id': 1}

url = 'http://localhost:8000/entries/'
response = requests.post(url, data = json_data)
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/5e3f184c-974a-2720-305d-1878984668a5.png" width=80%>


## 6. ペジネーション


一覧を取得するAPIはこのままだと全レコードを送ってしまうのでえらい事になります。定番の対処方法がペジネーションすなわちページ分けです


### 6-1. settings.pyに設定を書く


使用するPaginationクラスと、1回に表示するレコード数を指定すればペジネーションしてくれます


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


### 6-2. 取得してみる


指定した数だけ返してくれます。nextとpreviousのURLも付いてくるので次に進むときはそのURLで呼ぶだけです。これは本当にありがたいですね


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/10db234d-8a9c-f9db-2c1b-b1744cf2d2a8.png)


## 7. フィルターをつかう


<a href="https://django-filter.readthedocs.io/en/main/guide/usage.html#the-filter">django-filter</a>を使うとユーザーID=1に限定するなど、フィルタをかけて一覧を取得することができます


### 7-1. settings.pyにdjango_filtersを登録する


django_filtersをINSTALLED_APPSに追加します


```python:/django_rest_framework_test/settings.py
INSTALLED_APPS = [
    ...
    'django_filters',
]
```


### 7-2. Viewにフィルタを設定する


django_filters.FilterSetを作ってViewSetに入れてやるとフィルタを適用することができます


```python:/blog/views.py
import django_filters
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
from .models import User, Entry
from .serializer import UserSerializer, EntrySerializer

class EntryFilter(django_filters.FilterSet):
    author = django_filters.NumberFilter()
    status = django_filters.CharFilter()

    class Meta:
        model = Entry
        fields = ('author', 'status')

class EntryViewSet(viewsets.ModelViewSet):
    queryset = Entry.objects.all()
    serializer_class = EntrySerializer
    filter_backends = [DjangoFilterBackend]
    filterset_class = EntryFilter
```


### 7-3. 取得してみる


?author=1&status=public などでフィルタをかけられるようになりました


```python:
url = 'http://localhost:8000/entries?author=1&status=public'
res = requests.get(url)
print(res)
print(res.status_code)
values = json.loads(res.text)
values
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/fd4fc37b-5037-9e23-155d-358c64da36f2.png)


## まとめ


公式ドキュメントが分かりにくくて苦戦しましたが、フレームワークはすごく良く出来ていてかなり楽に実装できそうです。まだ把握出来てない機能がかなりありそうなので使っていくのが楽しみです。レッツトライ