Skip to content

DRF 框架

drf-tutorial

Restful API 最佳实践

  • 协议
  • 域名
  • 版本
  • 路径
  • HTTP动词
  • 过滤信息(Filtering)
  • 状态码(Status Codes)
  • 错误处理(Error handling)
  • 返回结果
  • Hypermedia API

HTTP 请求方法详解

  • GET (SELECT): 从服务器取出资源(一项或多项)
  • POST (CREATE): 在服务器新建一个资源
  • PUT (UPDATE): 在服务器更新资源(客户端提供完整资源)
  • PATCH (UPDATE): 在服务器更新资源(客户端提供部分资源)
  • DELETE (DELETE): 从服务器删除资源
  • HEAD: 获取资源的元数据
  • OPTIONS: 获取信息,关于资源的哪些属性是客户端可以改变的

创建项目

bash
$ pip install django djangorestframework        # 安装项目依赖
$ django-admin startproject drf_tutorial        # 创建项目
$ cd drf_tutorial                               # 进入项目目录
$ python manage.py startapp course              # 创建一个课程应用
# 先去修改 `settings.py`, 配置文件完整信息全部在下面: (统一接下来的规范)
$ python manage.py makemigrations               # 生成迁移文件
$ python manage.py migrate                      # 迁移数据
$ python manage.py createsuperuser              # 创建后台超级管理员
$ python manage.py runserver localhost:8000     # 启动数据库
  • 修改 settings.py 配置文件信息如下:
详情:
python
"""
Django settings for drf_tutorial project.

Generated by 'django-admin startproject' using Django 5.0.6.

For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import os.path
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-7m855cg9s1&vk*m7r3emn4_mjvr3yw=ygn+9o2)m*j#jt2&=ph"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']


# Application definition

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",  # 添加 rest_framework 框架
    "rest_framework.authtoken",  # 启用 DRF 自带的Token认证, 需要迁移数据, 生成存储 token 的表
    "course",  # 添加应用
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "drf_tutorial.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "drf_tutorial.wsgi.application"


# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}


# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]


# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/

LANGUAGE_CODE = "zh-hans"

TIME_ZONE = "Asia/Shanghai"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/

STATIC_URL = "static/"

# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
STATIC_ROOT = BASE_DIR / "static"

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'staticfiles'),
]

# DRF的全局配置
REST_FRAMEWORK = {
    # DRF 自带的分页的类
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    # 每页显示的个数
    "PAGE_SIZE": 10,
    # 时间显示的格式
    "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S",     # 年月日 时分秒
    # drf 返回 response 的时候, 使用哪一个 render 类
    "DEFAULT_RENDERER_CLASSES": [
        "rest_framework.renderers.JSONRenderer",
        "rest_framework.renderers.BrowsableAPIRenderer",
    ],
    # 如何解析 Request
    "DEFAULT_PARSER_CLASSES": [
        "rest_framework.parsers.JSONParser",    # 解析 json
        "rest_framework.parsers.FormParser",    # 解析 form
        "rest_framework.parsers.MultiPartParser",   # 解析文件
    ],
    # 权限认证方式
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
        # "rest_framework.permissions.IsAdminUser",
    ],
    # 认证方式
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.BasicAuthentication",    # 用户名密码认证
        "rest_framework.authentication.SessionAuthentication",  # session 认证
        "rest_framework.authentication.TokenAuthentication",    # token 认证, 如果使用 token 认证, 就需要在 app 中启用rest自带的Token认证功能
    ],
}
python
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('api-auth/', include('rest_framework.urls')),  # drf 的登录退出功能
    path("admin/", admin.site.urls),
]

直接 copy 覆盖自己的配置文件即可

DRF 组件模块

DRF进阶与实战

创建模型

详情:
python
from django.db import models
from django.conf import settings

class Course(models.Model):
    name = models.CharField(max_length=255, unique=True, help_text='课程信息', verbose_name='课程信息')
    introduction = models.TextField(help_text='课程介绍', verbose_name='课程介绍')
    teacher = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, help_text='课程讲师',
                                verbose_name='讲师')
    price = models.DecimalField(max_digits=6, decimal_places=2, help_text='课程价格', verbose_name='课程价格')
    create_at = models.DateTimeField(auto_now_add=True, help_text='创建时间', verbose_name='创建时间')
    update_at = models.DateTimeField(auto_now=True, help_text='更新时间', verbose_name='更新时间')

    class Meta:
        verbose_name = '课程信息'
        verbose_name_plural = verbose_name
        ordering = ('price', )

    def __str__(self):
        return self.name
python
from django.contrib import admin
from .models import Course


@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
    list_display = ('name', 'price', 'introduction', 'teacher')
    search_fields = list_display
    list_filter = list_display

⚠️ 执行一下数据库迁移

可以在pycharm的控制台通过下面的方式调试模型类

python
from course.models import Course
from django.core import serializers

serializers.serialize('json', Course.objects.all())     # 获取所有课程信息的序列化结果

⚠️django自带的序列化的不足

  1. 验证处理 request.data
  2. 验证器的参数
  3. 同时序列化多个对象
  4. 序列化的过程中添加上下文
  5. 无效的数据异常处理

DRF却包括了以上所有功能

DRF视图开发RESTful API接口

  • 原生django开发视图
python
# views.py

import json

from django.http import HttpResponse, JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt

course_dict = {
    'name': '课程名称',
    'introduction': '课程介绍',
    'price': 11.1
}


# Django原生的 FBV 编写API接口
@csrf_exempt
def course_list(request):
    if request.method == 'GET':
        # return HttpResponse(json.dumps(course_dict, content_type='application/json'))
        return JsonResponse(course_dict)  # 两者等价

    if request.method == 'POST':
        course = json.loads(request.body.decode('utf-8'))
        # return JsonResponse(course, safe=False)
        return HttpResponse(json.dumps(course, content_type='application/json'))


# Django CBV 编写API接口
@method_decorator(csrf_exempt, name='dispatch')
class CourseList(View):
    def get(self, request):
        return JsonResponse(course_dict)

    @csrf_exempt
    def post(self, request):
        course = json.loads(request.body.decode('utf-8'))
        return HttpResponse(json.dumps(course), content_type='application/json')

# 分页、排序、认证、权限、限流等等

函数式编程 Function Based View

类视图 Classed Based View

通用类视图 Generic Classed Based View

DRF的视图集 viewsets

from drf_tutorial.urls import urlpatterns

DRF API开发教程

移除所有第三方包依赖

bash
$ pip freeze | xargs pip uninstall -y     # 卸载所有第三方包
$ pip cache purge                         # 清理包下载缓存

使用原生 Django 开发API接口

python
import json
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.views import View


# Django FBV 编写API接口
@csrf_exempt
def course_list(request):
    course_dict = {
        'name': 'Django',
        'price': 0.11
    }
    if request.method == 'GET':
        return HttpResponse(json.dumps(course_dict), content_type='application/json')
        # return JsonResponse(course_dict)

    if request.method == 'POST':
        course = json.loads(request.body.decode('utf-8'))
        return HttpResponse(json.dumps(course), content_type='application/json')
        # return JsonResponse(course, safe=False)

# Django CBV 编写API接口
@method_decorator(csrf_exempt, name='dispatch')
class CourseList(View):
    """
    编写对应的方法执行的视图函数
    """
    def dispatch(self, request, *args, **kwargs):
        print('这个方法用于根据请求方法来控制视图分发')
        return super().dispatch(request, *args, **kwargs)

    def get(self, request):
        pass

    def post(self, request):
        pass

使用Django原生的方式编写API接口, 很多东西需要从零开始实现, 比如: 分页、排序、认证、权限、限流等

使用 Django REST Framework 开发API接口

python
from rest_framework.decorators import api_view

"""一、函数式编程: Function Based View"""
@api_view(['GET', 'POST'])
def course_list(request):
    """
    获取所有课程或新增一个课程
    """
    course_dict = {
        'name': 'Django',
        'price': 0.99
    }
    if request.method == 'GET':
        pass
    elif request.method == 'POST':
        pass

使用 DRF的APIView来开发接口

1. 创建模型

python
# todo/models.py
from django.db import models

class Todo(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    done = models.BooleanField(default=False)

    def __str__(self):
        return self.name

2. 创建视图

  • 2.1 使用 Python 注释风格编写API文档描述
python
# todo/views.py
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import serializers
from rest_framework import status

from .models import Todo


class TodoSerializer(serializers.Serializer):
    """ 某个模型的序列化器 """
    
    id = serializers.IntegerField()
    name = serializers.CharField(max_length=32)
    done = serializers.BooleanField()

    def create(self, validated_data):
        # 将实例对象返回出去
        instance = Todo.objects.create(**validated_data)
        return instance

    def update(self, instance, validated_data):
        # 更新实例对象
        Todo.objects.filter(pk=instance.pk).update(**validated_data)
        return instance



class TodosView(APIView):
    """
    查询集列表数据 & 创建新对象
    """
    def get(self, request):
        """
        获取查询集数据列表
        """
        # 查询实例对象列表
        todos = Todo.objects.all()
        # 序列化实例对象列表, 传入 instance 表示要将查询集进行序列化为 JSON 数据
        serializer = TodoSerializer(instance=todos, many=True)
        return Response(serializer.data)

    def post(self, request):
        """
        创建一条新对象
        """
        # 没有传 instance 参数, 会走新建对象的逻辑
        serializer = TodoSerializer(data=request.data)
        # 对字段规则进行校验
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        else:
            # 返回字段校验错误信息
            return Response(serializer.errors)


class TodoDetailView(APIView):
    """
    单条数据对象: 查 & 删 & 改
    
    GET: 查询单条数据对象
    PUT: 更新单条数据对象
    DELETE: 删除单条数据对象
    """

    def get(self, request, pk):
        instance = Todo.objects.get(pk=pk)
        serializer = TodoSerializer(instance=instance)
        return Response(serializer.data)


    def put(self, request, pk):
        todo = Todo.objects.get(pk=pk)
        # 创建只需要传入 data 参数, 更新需要传入更新的参考数据源和需要更新的实例对象
        serializer = TodoSerializer(instance=todo, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.validated_data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        Todo.objects.get(pk=pk).delete()
        return Response()
  • 2.2 使用 markdown 风格编写API文档描述(需额外配置)

安装依赖

bash
$ pip install markdown

编写 & 配置渲染器

python
# APIRenderers
# todo/renderers.py
from rest_framework.renderers import BaseRenderer
import markdown

class MarkdownRenderer(BaseRenderer):
    media_type = 'text/html'
    format = 'html'
    charset = 'utf-8'

    def render(self, data, media_type=None, renderer_context=None):
        # 将 Markdown 转换为 HTML
        return markdown.markdown(data)
python
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
        'todo.renderers.MarkdownRenderer'           # 添加自定义渲染器
    ],
}
  • API示例代码
python
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import serializers
from rest_framework import status

from .models import Todo


class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        model = Todo
        fields = ('id', 'name', 'done')


class TodosView(APIView):
    """
    Todo 列表操作

    - **GET**: 获取所有 Todo 项的列表。
    - **POST**: 创建一个新的 Todo 项。

    ### GET 请求
    - **URL**: `/todos/`
    - **响应**:
      - **200 OK**: 返回一个包含所有 Todo 项的列表。

    ### POST 请求
    - **URL**: `/todos/`
    - **请求体**:
      - `name`: Todo 项的名称 (必填)
      - `done`: 是否已完成 (可选,默认为 False)
    - **响应**:
      - **201 Created**: 创建成功,返回新创建的 Todo 项的信息。
      - **400 Bad Request**: 请求体验证失败。
    """

    def get(self, request):
        """
        获取所有 Todo 项的列表。

        - **URL**: `/todos/`
        - **响应**:
          - **200 OK**: 返回一个包含所有 Todo 项的列表。
        """
        todos = Todo.objects.all()
        serializer = TodoSerializer(instance=todos, many=True)
        return Response(serializer.data)

    def post(self, request):
        """
        创建一个新的 Todo 项。

        - **URL**: `/todos/`
        - **请求体**:
          - `name`: Todo 项的名称 (必填)
          - `done`: 是否已完成 (可选,默认为 False)
        - **响应**:
          - **201 Created**: 创建成功,返回新创建的 Todo 项的信息。
          - **400 Bad Request**: 请求体验证失败。
        """
        serializer = TodoSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class TodoDetailView(APIView):
    """
    Todo 项的详细操作

    - **GET**: 获取指定 ID 的 Todo 项详情。
    - **PUT**: 更新指定 ID 的 Todo 项。
    - **DELETE**: 删除指定 ID 的 Todo 项。

    ### GET 请求
    - **URL**: `/todos/<int:pk>/`
    - **响应**:
      - **200 OK**: 返回指定 Todo 项的详细信息。

    ### PUT 请求
    - **URL**: `/todos/<int:pk>/`
    - **请求体**:
      - `name`: Todo 项的名称 (可选)
      - `done`: 是否已完成 (可选)
    - **响应**:
      - **200 OK**: 更新成功,返回更新后的 Todo 项信息。
      - **400 Bad Request**: 请求体验证失败。

    ### DELETE 请求
    - **URL**: `/todos/<int:pk>/`
    - **响应**:
      - **204 No Content**: 删除成功。
    """

    def get(self, request, pk):
        """
        获取指定 ID 的 Todo 项详情。

        - **URL**: `/todos/<int:pk>/`
        - **响应**:
          - **200 OK**: 返回指定 Todo 项的详细信息。
        """
        instance = Todo.objects.get(pk=pk)
        serializer = TodoSerializer(instance=instance)
        return Response(serializer.data)

    def put(self, request, pk):
        """
        更新指定 ID 的 Todo 项。

        - **URL**: `/todos/<int:pk>/`
        - **请求体**:
          - `name`: Todo 项的名称 (可选)
          - `done`: 是否已完成 (可选)
        - **响应**:
          - **200 OK**: 更新成功,返回更新后的 Todo 项信息。
          - **400 Bad Request**: 请求体验证失败。
        """
        todo = Todo.objects.get(pk=pk)
        serializer = TodoSerializer(todo, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.validated_data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        """
        删除指定 ID 的 Todo 项。

        - **URL**: `/todos/<int:pk>/`
        - **响应**:
          - **204 No Content**: 删除成功。
        """
        Todo.objects.get(pk=pk).delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

3. 配置路由

python
# todo/urls.py
from django.urls import path, re_path
from .views import TodosView, TodoDetailView

urlpatterns = [
    # 查询集列表数据 & 创建新对象
    path('todos/', TodosView.as_view()),
    # 单条数据对象: 查 & 删 & 改
    re_path('todos/(\d+)/', TodoDetailView.as_view())
]
python
# 总路由 urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.documentation import include_docs_urls

urlpatterns = [
    path('admin/', admin.site.urls),
    path('todo/', include('todo.urls')),
    path('docs/', include_docs_urls(title="API文档配置示例")),
]

生成 API 文档

1. 使用 coreapi 生成 API 文档

  • 配置 settings.py
python
# settings.py

INSTALLED_APPS = [
    # ...
    'todo',
    'coreapi',
    'rest_framework',
]

# ...

# DRF 的全局配置
REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
}
  • 配置路由
python
# 总路由下面添加 API 文档路由
from django.urls import path, include
from rest_framework.documentation import include_docs_urls

urlpatterns = [
    # ...
    path('api/', include('todo.urls')),
    path('docs/', include_docs_urls(title="API文档配置示例")),
]

访问: http://127.0.0.1:8000/docs/

2. 使用 drf-yasg 生成 API 文档

  • 安装依赖包:
bash
$ pip install drf-yasg
  • 配置 settings.py:
python
INSTALLED_APPS = [
    # ...
    'drf_yasg',
]
  • 配置路由:
python
# 总路由下面添加 API 文档路由 urls.py
from django.contrib import admin
from django.urls import path, include, re_path
from rest_framework.documentation import include_docs_urls

from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi

# 配置 swagger
schema_view = get_schema_view(
   openapi.Info(
      title="Swagger API",
      default_version='v1',
      description="API文档配置示例详情描述",
      terms_of_service="https://ZhouYu2156.github.io/",
      contact=openapi.Contact(email="contact@snippets.local"),
      license=openapi.License(name="BSD License"),
   ),
   public=True,
   permission_classes=[permissions.AllowAny],
)

urlpatterns = [
    # ...
    path('admin/', admin.site.urls),
    path('api/', include('todo.urls')),
    path('docs/', include_docs_urls(title="API文档配置示例")),
    re_path(r'^swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
    re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
    re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]