1. 程式人生 > >利用 Django REST framework 編寫 RESTful API

利用 Django REST framework 編寫 RESTful API

最近在玩 Django,不得不說 rest_framework 真乃一大神器,可以輕易的甚至自動化的搞定很多事情,比如:

  • 自動生成符合 RESTful 規範的 API
    • 支援 OPTION、HEAD、POST、GET、PATCH、PUT、DELETE
    • 根據 Content-Type 來動態的返回資料型別(如 text、json)
  • 生成 browserable 的互動頁面(自動為 API 生成非常友好的瀏覽器頁面)
  • 非常細粒度的許可權管理(可以細粒度到 field 級別)

示意圖

安裝

$ pip install djangorestframework
$ pip install markdown

概述

Django Rest framework 的流程大概是這樣的

  1. 建立 Models
  2. 依靠 Serialiers 將資料庫取出的資料 Parse 為 API 的資料(可用於返回給客戶端,也可用於瀏覽器顯示)
  3. ViewSet 是一個 views 的集合,根據客戶端的請求(GET、POST等),返回 Serialiers 處理的資料
    • 許可權 Premissions 也在這一步做處理
  4. ViewSet 可在 Routers 進行註冊,註冊後會顯示在 Api Root 頁上
  5. 在 urls 裡註冊 ViewSet 生成的 view,指定監聽的 url

希望全面細緻瞭解的人請移步去看官方文件,我這裡就不一步步的細說了,而是分塊來進行介紹

準備工作 & Models

讓我們來寫個小專案練練手

  1. 先用 manage.py startproject rest 來生成一個專案
  2. 再用 manage.py createsuperuser 建立使用者(後面許可權管理會用到)
  3. 初始化資料庫 manage.py migrate

然後當然是編寫 models,為了展示 rest_framework 的強大之處,我給 models 定義了一個自定義的 field

# myproject/myapp/models.py


#! /usr/bin/env python
# -*- coding: utf-8
from __future__ import unicode_literals, absolute_import
import cPickle as pickle

from django.db import models
from django.contrib.auth.models import User


class SerializedField(models.TextField):

    """序列化域
    用 pickle 來實現儲存 Python 物件
    """
    __metaclass__ = models.SubfieldBase  # 必須指定該 metaclass 才能使用 to_python

    def validate(self, val):
        raise isinstance(val, basestring)

    def to_python(self, val):
        """從資料庫中取出字串,解析為 python 物件"""
        if val and isinstance(val, unicode):
            return pickle.loads(val.encode('utf-8'))

        return val

    def get_prep_value(self, val):
        """將 python object 存入資料庫"""
        return pickle.dumps(val)


class MyModel(models.Model):

    created_at = models.DateTimeField(auto_now_add=True)
    # 注意這裡建立了一個外來鍵
    owner = models.ForeignKey(User, related_name='mymodels')
    field = models.CharField(max_length=100)
    options = SerializedField(max_length=1000, default={})

Serializers

定義好了 Models,我們可以開始寫 Serializers,這個相當於 Django 的 Form

# myproject/myapp/serializers.py

#! /usr/bin/env python
# -*- coding: utf-8
from __future__ import unicode_literals, absolute_import
import json

from django.contrib.auth.models import User
from rest_framework import serializers

from ..models import MyModel
from .fields import MyCustField


class MyCustField(serializers.CharField):
    """為 Model 中的自定義域額外寫的自定義 Serializer Field"""

    def to_representation(self, obj):
        """將從 Model 取出的資料 parse 給 Api"""
        return obj

    def to_internal_value(self, data):
        """將客戶端傳來的 json 資料 parse 給 Model"""
        return json.loads(data.encode('utf-8'))


class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User  # 定義關聯的 Model
        fields = ('id', 'username', 'mymodels')  # 指定返回的 fields

    # 這句話的作用是為 MyModel 中的外來鍵建立超連結,依賴於 urls 中的 name 引數
    # 不想要這個功能的話完全可以註釋掉
    mymodels = serializers.HyperlinkedRelatedField(
        many=True, queryset=MyModel.objects.all(),
        view_name='model-detail'
    )


class MySerializer(serializers.ModelSerializer):

    options = MyCustField(
        max_length=1000, style={'base_template': 'textarea.html'},
    )

    class Meta:
        model = MyModel
        fields = ('id', 'owner', 'field', 'options')
        read_only_fields = ('owner',)  # 指定只讀的 field

    def create(self, validated_data):
        """響應 POST 請求"""
        # 自動為使用者提交的 model 新增 owner
        validated_data['owner'] = self.context['request'].user
        return MyModel.objects.create(**validated_data)

    def update(self, instance, validated_data):
        """響應 PUT 請求"""
        instance.field = validated_data.get('field', instance.field)
        instance.save()
        return instance

ViewSet

定義好了 Serializers,就可以開始寫 viewset 了

其實 viewset 反而是最簡單的部分,rest_framework 原生提供了四種 ViewSet

  • ViewSet
  • GenericViewSet
    • 繼承於 GenericAPIView
  • ModelViewSet
    • 自身提供了六種方法
    • list
    • create
    • retrieve
    • update
    • partial_update
    • destroy
  • ReadOnlyModelViewSet

我比較喜歡用 ModelViewSet,然後再用 Premissions 來管理許可權

# myproject/myapp/views.py

#! /usr/bin/env python
# -*- coding: utf-8
from __future__ import unicode_literals, absolute_import

from django.contrib.auth.models import User
from rest_framework import permissions, viewsets, renderers
from rest_framework.decorators import (
    permission_classes, detail_route
)
from rest_framework.response import Response

from .serializers import MySerializer, UserSerializer
from .models import MyModel


class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    # 指定許可權,下面馬上講到
    permission_classes = (permissions.IsAuthenticated,)


class ModelViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MySerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

    @detail_route(renderer_classes=[renderers.StaticHTMLRenderer])
    def plaintext(self, request, *args, **kwargs):
        """自定義 Api 方法"""
        model = self.get_object()
        return Response(repr(model))

我在 ModelViewSet 中自定義了方法 plaintext,rest_framework 中對於自定義的 viewset 方法提供了兩種裝飾器

  • list_route
  • detail_route

區別就是 list_route 的引數不包含 pk(對應 list),而 detail_route 包含pk(對應 retrieve)

看一段程式碼就懂了

@list_route(methods=['post', 'delete'])
def custom_handler(self, request):
    pass


@detail_route(methods=['get'])
def custom_handler(self, request, pk=None):
    pass

Filters

前面根據 serializers 和 viewset 我們已經可以很好的提供資料介面和展示了。但是有時候我們需要通過 url引數 來對資料進行一些排序或過濾的操作,為此,rest-framwork 提供了 filters 來滿足這一需求。

全域性filter

可以在 settings 裡指定應用到全域性的 filter:

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',)
}

viewset 的 filter

也可以為 viewset 分別指定 filter,方法就是在定義 viewset 的時候定義一個名為filter_backend 的類變數:

class UserListView(generics.ListAPIView):
    queryset = User.objects.all()
    serializer = UserSerializer
    filter_backends = (filters.DjangoFilterBackend,)

預設的 filter

rest-framework 提供了幾個原生的 filter:

  • SearchFilter
filter_backends = (filters.SearchFilter,)
search_fields = ('username', 'email')  # 指定搜尋的域

請求 http://example.com/api/users?search=russell

  • OrderingFilter
filter_backends = (filters.OrderingFilter,)
ordering_fields = ('username', 'email')

請求 http://example.com/api/users?ordering=account,-username

自定義 filter

自定義 filter 非常簡單,只需要定義 filter_queryset(self, request, queryset, view) 方法,並返回一個 queryset 即可。

直接貼一個我寫的例子:

class NodenameFilter(filters.BaseFilterBackend):

    """根據 nodename 來刪選
      [nodename]: NeiWang
    """

    def filter_queryset(self, request, queryset, view):
        nodename = request.QUERY_PARAMS.get('nodename')
        if nodename:
            return queryset.filter(nodename=nodename)
        else:
            return queryset

如果引數匹配有誤,想要丟擲異常的話,也可以自定義 APIError,舉個例子:

from rest_framework.exceptions import APIException


class FilterError(APIException):
    status_code = 406
    default_detail = 'Query arguments error!'

然後在 viewset 裡直接丟擲 raise FilterError 即可。

Premissions

顧名思義就是許可權管理,用來給 ViewSet 設定許可權,使用 premissions 可以方便的設定不同級別的許可權:

  • 全域性許可權控制
  • ViewSet 的許可權控制
  • Method 的許可權
  • Object 的許可權

被 premission 攔截的請求會有如下的返回結果:

  • 當用戶已登入,但是被 premissions 限制,會返回 HTTP 403 Forbidden
  • 當用戶未登入,被 premissions 限制會返回 HTTP 401 Unauthorized

預設的許可權

rest_framework 中提供了七種許可權

  • AllowAny # 無限制
  • IsAuthenticated # 登陸使用者
  • IsAdminUser # Admin 使用者
  • IsAuthenticatedOrReadOnly # 非登入使用者只讀
  • DjangoModelPermissions # 以下都是根據 Django 的 ModelPremissions
  • DjangoModelPermissionsOrAnonReadOnly
  • DjangoObjectPermissions

全域性許可權控制

在 settings.py 中可以設定全域性預設許可權

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.AllowAny',
    ),
}

ViewSet 的許可權

可以設定 permission_classes 的類屬性來給 viewset 設定許可權,restframework 會檢查元組內的每一個 premission,必須要全部通過才行。

class UserViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    # 設定許可權,是一個元組
    permission_classes = (permissions.IsAuthenticated,)

自定義許可權

Premissions 可以非常方便的定製,比如我就自己寫了一個只允許 owner 編輯的許可權

# myproject/myapp/premissions.py

#! /usr/bin/env python
# -*- coding: utf-8
from __future__ import unicode_literals, absolute_import

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):

    def has_permission(self, request, view):
        """針對每一次請求的許可權檢查"""
        if request.method in permissions.SAFE_METHODS:
            return True

    def has_object_permission(self, request, view, obj):
        """針對資料庫條目的許可權檢查,返回 True 表示允許"""
        # 允許訪問只讀方法
        if request.method in permissions.SAFE_METHODS:
            return True

        # 非安全方法需要檢查使用者是否是 owner
        return obj.owner == request.user

urls & routers

# myproject/myapp/urls.py

#! /usr/bin/env python
# -*- coding: utf-8
from __future__ import unicode_literals, absolute_import

from django.conf.urls import url, patterns, include
from rest_framework.routers import DefaultRouter

from . import views


# as_view 方法生成 view
# 可以非常方便的指定 `{Http Method: View Method}`
user_detail = views.UserViewSet.as_view({'get': 'retrieve'})
user_list = views.UserViewSet.as_view({'get': 'list', 'post': 'create'})

# plaintext 是我的自定義方法,也可以非常方便的指定
modal_plain = views.ModelViewSet.as_view({'get': 'plaintext'})
model_detail = views.ModelViewSet.as_view({'get': 'retrieve', 'post': 'create'})
model_list = views.ModelViewSet.as_view({'get': 'list', 'post': 'create'})

# router 的作用就是自動生成 Api Root 頁面
router = DefaultRouter()
router.register(r'models', views.ModelViewSet)
router.register(r'users', views.UserViewSet)


# 不要忘了把 views 註冊到 urls 中
urlpatterns = patterns(
    '',
    url(r'^', include(router.urls)),  # Api Root
    url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
    url(r'^models/(?P<pk>[0-9]+)/$', model_detail, name='model-detail'),
    url(r'^models/(?P<pk>[0-9]+)/plain/$', modal_plain, name='model-plain'),
    url(r'^models/$', model_list, name='model-list'),
    url(r'^users/$', user_list, name='user-list'),
    url(r'^users/(?P<pk>[0-9]+)/$', user_detail, name='user-detail'),
)

時間倉促,就介紹這些,以後有空再介紹一下在 Django 用 JWT 作為身份憑證。下面是一些效果圖

  • Api Root

  • Users