Django REST framework カスタマイズ方法 - チュートリアルの補足

## :barber:  この記事について
Django REST frameworkはデータベース連動のREST APIを最小限の労力で作れるフレームワークです。
すでに日本語のチュートリアルの良記事がいくつかあって、内容通りに進めればサービスの公開まで迷わずに進めることができます。

しかし業務で使うとなるとチュートリアルの内容では物足りなく、機能やセキュリティの面でもう少し複雑な設定が必要になります。公式サイトで調べたカスタマイズ方法をまとめておきます。

##:beginner: チュートリアル集

Django REST Framework の基本が学べます。どれもわかりやすいです。爆速！

[Django REST Frameworkを使って爆速でAPIを実装する](https://qiita.com/kimihiro_n/items/86e0a9e619720e57ecd8)
[Django REST frameworkで爆速API開発 導入編 - OfferBox Tech Blog](http://i-plug-tech.hatenablog.com/entry/2016/10/27/110000)
[Django REST Frameworkに再挑戦 その1 - Djangoroidの奮闘記]
(http://pythonskywalker.hatenablog.com/entry/2017/01/05/082244)
[らっちゃいブログ Django REST framework 超入門](http://racchai.hatenablog.com/entry/2016/04/12/Django_REST_framework_%E8%B6%85%E5%85%A5%E9%96%80)

## :airplane_arriving: チュートリアルの復習

### 基本クラスについて

#### 概要図

![image.png](https://qiita-image-store.s3.amazonaws.com/0/217094/7ef3221b-2a66-18c1-8a8f-3eba76a6e3d6.png)

#### 主要クラスの説明


|クラス名|定義場所|役割|
|:--|:--|:--|
| **Router(DefaultRouter)** |urls.py |リクエストのHTTPメソッドとエンドポイントを判別し、対応するViewSetのアクションを実行する|
| ViewSet| views.py または apis.py | WebAPIの本体。RestAPIのCRUD操作に対応する６種類のアクション（メソッド）が事前に用意されており、必要な部分をオーバーライドして使う|
| **ModelViewSet**| views.py または apis.py | ViewSetの拡張クラス。**特定のmodelを指定すると、modelのCRUD操作がViewSetのアクションとして暗黙的に実装される**|
|Serializer|serializers.py|オブジェクトとJSON(XML)間の変換ルールやバリデーション処理を定義する。|
|**ModelSerializer**|serializers.py|Serializerの拡張クラス。特定のmodelを指定すると、**modelのフィールド設定が、Serializerのフィールド設定として暗黙的に実装される**|


### Viewsetで提供されるアクション一覧

参考：http://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions

|役割|エンドポイント|HTTPメソッド|アクション名|
|:--|:--|:--|:--|
|リソースの取得（複数）|リソース名/|GET|list
|リソースの作成|リソース名/|POST|create
|リソースの取得（個別）|リソース名/id/|GET|retrieve
|リソースの更新（全部）|リソース名/id/|PUT|update
|リソースの更新（一部）|リソース名/id/|PATCH|partial_update
|リソースの削除|リソース名/id/|DELETE|destroy

デコレータ「@list_route、@detail_route」を使ってViewSetに独自のアクションを追加することも可能である（後述）。

##:roller_coaster: 作成方針

チュートリアルのように、ModelViewSet、ModelSerializerを活用してシンプルな構成にするのがよろしい。そこから要件に合わせて機能の加減やセキュリティの設定をする。

## :clipboard:基本的な作業手順

チュートリアルと同じでＯＫ

１．プロジェクトを構成する
２．modelを定義し、データベースに反映する
３．modelを元にModelSerializer、ModelViewSetを作成し、urlpatterns、Router、を設定して基本動作を確認する。
４．要件に合わせる形でModelSerializerとModelViewSetをカスタマイズする

## :hammer_pick:カスタマイズ方法

###:military_medal: ModelSerializerのカスタマイズ

####:star: カスタマイズ前に現状を確認する

ModelSerializerはmodelの設定を引用するため自前のコーディングは最小限でよい。
まずは引用後にどのような設定がなされるのかをシェルより確認し、過不足の分だけコーディングしよう。

```py:
# manage.py shellより 
>>> from myapp.serializers import AccountSerializer
>>> serializer = AccountSerializer()
>>> print(repr(serializer))
AccountSerializer():
    id = IntegerField(label='ID', read_only=True)
    name = CharField(allow_blank=True, max_length=100, required=False)
    owner = PrimaryKeyRelatedField(queryset=User.objects.all())
```

参考：http://www.django-rest-framework.org/api-guide/serializers/#inspecting-a-modelserializer

####:star: カスタマイズ項目


|項目|概要|
|:--|:--|
|追加フィールド|入力・出力値を変換する場合など、modelのフィールドに対応しない一時的なフィールドを追加する。<br>例：Childs = serializers.IntegerField(read_only=True) <br>※1| 
|フィールド属性の変更|fields アクセス可<br>exclude アクセス不可<br>read_only_fields 読み取り専用<br>extra_kwargs 既存のフィールドに属性を追加する<br>※2<br>|
|デシリアライザ|create()、update()をオーバーライドすることで単純なフィールド対応ではない登録、更新時の処理を定義できる。

※1 Serializerのフィールド設定書式にしたがう
参考：http://www.django-rest-framework.org/api-guide/fields/
※2 ModelSerializerでは既にModelフィールドにあわせて適切な設定がなされた状態なので、不要な設定を入れないように注意する。

##### サンプル：パスワードのフィールドをハッシュ値に変換して登録する

```py
class CreateUserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('email', 'username', 'password')
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = User(
            email=validated_data['email'],
            username=validated_data['username']
        )
        user.set_password(validated_data['password'])
        user.save()
        return user
```

参考：http://www.django-rest-framework.org/api-guide/serializers/#additional-keyword-arguments

### :alembic: ModelViewSetのカスタマイズ

#### :hamburger: 基本設定項目

|項目|概要|
|:--|:--|
|queryset|ソースとなるデータをクエリで指定|
|serializer_class|シリアライザの指定|
|permission_classes|パーミッションの指定|
|lookup_field|検索キーの指定（デフォルトはid）|

#### :pizza: パーミッションについて

参考：http://www.django-rest-framework.org/api-guide/permissions/

WebAPIへのアクセスを、指定したユーザーだけに制限することができる。
BasePermissionを継承したクラスで指定する。
以下の組み込みクラスが存在する。

・AllowAny 全許可（デフォルト ）
・IsAuthenticated 要ログイン
・IsAdminUser スーパーユーザーのみ
・IsAuthenticatedOrReadOnly ログインしていなければ参照(list,retrive)のみ。していれば全操作可。

##### :apple: パーミッションのカスタマイズ

BasePermissionを継承して独自のパーミッションを作成することもできる。

###### :cherries: サンプル：特定のIPアドレスだけに接続を許可する

```py
from rest_framework import permissions

class BlacklistPermission(permissions.BasePermission):
    """
    Global permission check for blacklisted IPs.
    """

    def has_permission(self, request, view):
        ip_addr = request.META['REMOTE_ADDR']
        blacklisted = Blacklist.objects.filter(ip_addr=ip_addr).exists()
        return not blacklisted
```

参考：http://www.django-rest-framework.org/api-guide/permissions/#examples

##### :poultry_leg: インスタンス単位（レコード単位）のアクセス制限

BasePermissionのhas_object_permissionをオーバーライドして、インスタンスレベルの制限も作ることができる。
:cherries: サンプル：参照以外の操作をインスタンスの所有者に制限する

```py
class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Object-level permission to only allow owners of an object to edit it.
    Assumes the model instance has an `owner` attribute.
    """

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Instance must have an attribute named `owner`.
        return obj.owner == request.user
```

参考：http://www.django-rest-framework.org/api-guide/permissions/#examples

ただし対象アクションはretrive,update,partial_update,destroyのみでlistは対象外。
ユーザーごとにフィルタ条件を変えるにはget_querysetのオーバーライド(後述)を使う。

#### :peach: 検索キーをUUIDにする
    
一般に公開するWebAPIではidを検索キーに使うと他のデータへのアクセスが容易に推測できてしまうのでUUIDを使うことが推奨されている。
    
参考：HerokuのAPIデザイン
https://deeeet.com/writing/2014/06/02/heroku-api-design/

UUIDを検索キーにするには、以下の設定をする
・モデルにUUIDFieldを追加
・ModelViewSetのlookup_fieldでフィールドを指定

```py
    class Sample(models.Model):

        uuid = models.UUIDField(
            db_index=True,
            default=uuid_lib.uuid4,
            editable=False)

    class SampleViewSet(viewsets.ModelViewSet):

        class META:
            lookup_field = 'uuid'
```


#### :watermelon: オーバーライドでのカスタマイズ方法

大きくわけて２種類ある。

- 既存メソッド（ジェネリッククラスのメソッド）をオーバーライドする

 - get_queryset()等をオーバーライドし、内部で場合分けの処理を入れる。アクション毎にパーミッションを変えるなどが可能になる。

- 登録・更新処理をオーバーライドする
 - 登録時に実行されるperform_createをオーバーライドし、追加処理を入れる。通知処理などはこの方法で可能。

以下サンプル。全て公式サイトより引用した。

##### :cherries:サンプル：アクション毎に使用するパーミッションを変える
get_permissionsをオーバーライドする

```py
def get_permissions(self):
    """
    Instantiates and returns the list of permissions that this view requires.
    """
    if self.action == 'list':
        permission_classes = [IsAuthenticated]
    else:
        permission_classes = [IsAdmin]
    return [permission() for permission in permission_classes]
```

参考：http://www.django-rest-framework.org/api-guide/viewsets/#viewset-actions

##### :cherries: サンプル：ユーザーが登録したデータのみを検索対象とする
クエリセットを取得する`get_queryset`をオーバーライドする

```py
from myapp.models import Purchase
from myapp.serializers import PurchaseSerializer
from rest_framework import generics

class PurchaseList(generics.ListAPIView):
    serializer_class = PurchaseSerializer

    def get_queryset(self):
        """
        This view should return a list of all the purchases
        for the currently authenticated user.
        """
        user = self.request.user
        return Purchase.objects.filter(purchaser=user)
```

参考：http://www.django-rest-framework.org/api-guide/filtering/#filtering-against-the-current-user

##### :cherries: サンプル：アクション毎にシリアライザを変更する

get_serializer_classをオーバーライドする

```py
def get_serializer_class(self):
    if self.request.user.is_staff:
        return FullAccountSerializer
    return BasicAccountSerializer
```

参考：http://www.django-rest-framework.org/api-guide/generic-views/#get_serializer_classself

以下はperform_XXのカスタマイズ例

##### :cherries: サンプル： ログインユーザーを所有者として使う


```py
def perform_create(self, serializer):
    serializer.save(user=self.request.user)
```

#### :cherries: サンプル： 登録済みのユーザーは再登録できなくする

```py
def perform_create(self, serializer):
    queryset = SignupRequest.objects.filter(user=self.request.user)
    if queryset.exists():
        raise ValidationError('You have already signed up')
    serializer.save(user=self.request.user)
```

#### :cherries: サンプル： 物理削除ではなく論理削除にする

```py
def perform_destroy(self, instance):
    instance.is_deleted = True
    instance.save
```

#### :cherries: サンプル： 更新時に通知処理を行う

```py
def perform_update(self, serializer):
    instance = serializer.save()
    send_email_confirmation(user=self.request.user, modified=instance)
```

### :hot_pepper:リレーション先の集計情報を表示する

annotateと集計表現を使ってmodelの結合先テーブルの情報も表示できる。
シリアライザに追加フィールドを置くのがポイント。

```py:models.py
from django.db import models

# Create your models here.
class Parent(models.Model):
    title = models.CharField(max_length=30)

class Child(models.Model):
    parent = models.ForeignKey(Parent, on_delete=models.CASCADE)
    value = models.IntegerField()
```

```py:serializers.py
from rest_framework import serializers
from .models import Parent

class ParentSerializer(serializers.ModelSerializer):
    # 子のレコード数。元のモデルにないフィールドはこのように別途定義する
    Childs = serializers.IntegerField(
        read_only=True
    )

    class Meta:
        model = Parent
        #上で定義したフィールドを追加
        fields = ('id','title','Childs')

```

```py:views.py
from django.shortcuts import render

# Create your views here.
from rest_framework import viewsets

from .models import Parent, Child
from .serializers import ParentSerializer

from django.db.models import Count

class ParentViewSet(viewsets.ModelViewSet):
    #クエリセットに集計フィールドを含める
    #get_querysetをオーバーライド（後述）してretrieveの時だけ集計させることも可能
    queryset = Parent.objects.annotate(Childs=Count('child'))
    serializer_class = ParentSerializer

```


### :tangerine: ModelViewに独自定義のアクションを追加する
デコレータ「@list_route、@detail_route」を使うことで独自に定義したアクションを追加できる。エンドポイントにアクションのメソッド名を追加してアクセスする。

@list_route リソース名/追加アクション名
@detail_route リソース名/pk/追加アクション名

参考：http://www.django-rest-framework.org/api-guide/viewsets/#marking-extra-actions-for-routing

公式サイトの例
@detail_route：指定ユーザーのパスワード更新
@list_route：最近ログインしたユーザー一覧の取得

```py
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework import viewsets
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
from myapp.serializers import UserSerializer, PasswordSerializer

class UserViewSet(viewsets.ModelViewSet):
    """
    A viewset that provides the standard actions
    """
    queryset = User.objects.all()
    serializer_class = UserSerializer

    @detail_route(methods=['post'])
    def set_password(self, request, pk=None):
        user = self.get_object()
        serializer = PasswordSerializer(data=request.data)
        if serializer.is_valid():
            user.set_password(serializer.data['password'])
            user.save()
            return Response({'status': 'password set'})
        else:
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

    @list_route()
    def recent_users(self, request):
        recent_users = User.objects.all().order('-last_login')

        page = self.paginate_queryset(recent_users)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(recent_users, many=True)
        return Response(serializer.data)
```

※ django-filterの設定は別途エントリを立てる