Help us understand the problem. What is going on with this article?

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

:barber: この記事について

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

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

:beginner: チュートリアル集

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

Django REST Frameworkを使って爆速でAPIを実装する
Django REST frameworkで爆速API開発 導入編 - OfferBox Tech Blog
Django REST Frameworkに再挑戦 その1 - Djangoroidの奮闘記
らっちゃいブログ Django REST framework 超入門

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

基本クラスについて

概要図

image.png

主要クラスの説明

クラス名 定義場所 役割
Router(DefaultRouter) urls.py リクエストのHTTPメソッドとエンドポイントを判別し、対応するViewSetのアクションを実行する
ViewSet views.py または apis.py WebAPIの本体。RestAPIのCRUD操作に対応する6種類のアクション(メソッド)が事前に用意されており、必要な部分をオーバーライドして使う
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:基本的な作業手順

チュートリアルと同じでOK

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

:hammer_pick:カスタマイズ方法

:military_medal: ModelSerializerのカスタマイズ

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

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

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

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

サンプル:パスワードのフィールドをハッシュ値に変換して登録する
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アドレスだけに接続を許可する
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: サンプル:参照以外の操作をインスタンスの所有者に制限する

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でフィールドを指定

    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: オーバーライドでのカスタマイズ方法

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

  • 既存メソッド(ジェネリッククラスのメソッド)をオーバーライドする

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

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

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

:cherries:サンプル:アクション毎に使用するパーミッションを変える

get_permissionsをオーバーライドする

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をオーバーライドする

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をオーバーライドする

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: サンプル: ログインユーザーを所有者として使う
def perform_create(self, serializer):
    serializer.save(user=self.request.user)

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

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: サンプル: 物理削除ではなく論理削除にする

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

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

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

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

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

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()
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')

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:最近ログインしたユーザー一覧の取得

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の設定は別途エントリを立てる

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away