Django后端开发学习笔记

基础篇

基础部分建议阅读官方文档:https://docs.djangoproject.com/zh-hans/4.2/

学习Django项目目录结构,以及如何创建、运行Django项目和APP,学习创建和操作数据库。

环境搭建

1
2
3
4
python3 -m pip install Django

# 如果没有django-admin命令,创建链接
sudo ln -s /Users/yinshouxiang/Library/Python/3.9/bin/django-admin /usr/local/bin/django-admin

创建&运行Django项目

创建mysite项目:

1
django-admin startproject mysite

指定目录创建Django项目:

1
django-admin startproject mysite .

注:Django项目名称不能包含-,项目目录可以包含-

运行mysite项目:

1
python3 manage.py runserver

mysite目录结构:

1
2
3
4
5
6
7
8
─ mysite
├── manage.py
└── mysite
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py

manage.py:用于管理 Django 项目

mysite/__init__.py :空文件,表明该目录为 Python 包

mysite/settings.py :项目的设置/配置,其中包括创建的应用程序、数据库用户名密码、根目录配置等

mysite/urls.py :项目的 url 接口声明

创建&运行APP

app是项目的子功能,一个项目可以创建多个app。

创建polls app:

1
2
3
mkdir -p src/v1                        
cd src/v1
python3 ../../manage.py startapp polls

创建app时, app目录不能包含.,比如v1.0。

运行polls app:

1
python3 ../../manage.py startapp polls

app目录结构:

1
2
3
4
5
6
7
8
9
── polls
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── tests.py
└── views.py

__init__.py:数据库地址、账号、密码等信息可以保存在init文件中

apps.py:app的配置

migrations:迁移文件,可忽略

models.py:数据库表定义

tests.py:单元测试

views.py:后端处理请求的核心代码,简单的示例代码如下:

1
2
3
4
5
# polls/views.py
from django.http import HttpResponse

def index(request):
return HttpResponse("Hello, world. You're at the polls index.")

urls.py:在app目录下可以添加urls.py文件,定义子接口

项目接口配置

在urls.py文件中配置项目的后端接口:

1
2
3
4
urlpatterns = [
path("api/v1/polls/article/", include('src.v1.polls.article.urls')),
path("api/v1/polls/comment/", include('src.v1.polls.comment.urls'))
]

在app(如src.v1.polls.article)目录的urls.py文件中可以拼接子接口:

1
2
3
4
urlpatterns = [
path('', views.Articlet.as_view()),
path('/detail', views.ArticletDetail.as_view())
]

数据库相关

数据库配置

在Django项目的settings.py文件中配置数据库:

1
2
3
4
5
6
7
8
9
10
11
12
# mysite/settings.py

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'django_learn',
'USER': 'root',
'PASSWORD': 'yourpassword',
'HOST': '127.0.0.1',
'PORT': '3306',
}
}

数据库表定义&创建

在Django中,数据库表通过model进行定义,一个model对应一张数据表。

创建model:

创建model即是定义数据库表字段。

1
2
3
4
5
6
7
8
9
10
from django.db import models

class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")

class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)

CharField 表示字符字段, DateTimeField 表示日期时间,定义数据库字段类型;

question_text 或 pub_date 是数据库字段的名称;

max_length 用于限制并验证字段长度;

default 设置字段的默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 创建主键
id = models.AutoField(primary_key=True)
# 创建外键
# 第一个参数是指定关联哪个模型,第二个参数是在使用外键引用的模型数据被删除了,这个字段该如何处理
vehicle_id = models.ForeignKey(Vehicle, on_delete=models.DO_NOTHING, db_constraint=False)
'''
CASCADE:级联操作。如果外键对应的那条数据被删除了,那么这条数据也会被删除
PROTECT:受保护。即只要这条数据引用了外键的那条数据,那么就不能删除外键的那条数据。如果强行删除,Django就会报错
SET_NULL:设置为空。如果外键的那条数据被删除了,那么在本条数据上就将这个字段设置为空。如果设置这个选项,前提是要指定这个字段可以为空
SET_DEFAULT:设置默认值。如果外键的那条数据被删除了,那么本条数据上就将这个字段设置为默认值。如果设置这个选项,前提是要指定这个字段一个默认值
SET():如果外键的那条数据被删除了,那么将会获取SET函数中的值来作为这个外键的值
DO_NOTHING:不采取任何行为,一切全看数据库级别的约束
以上这些选项只是Django级别的,数据级别仍然是RESTRICT
'''
# 在 Django 中,外键关系通常是指向另一张表的主键字段(通常是 id 字段)。这是默认的行为,因此不需要手动指定外键指向的具体字段,Django 会自动使用目标表的主键字段。如果需要指定外键指向另一张表的非主键字段,可以在 ForeignKey 字段中使用 to_field 参数来指定目标字段

# 定义任务状态
class Task(models.Model):
PENDING = 0
RUNNING = 1
COMPLETED = 2
FAILED = 3

STATUS_CHOICES = [
(PENDING, '等待中'),
(RUNNING, '执行中'),
(COMPLETED, '完成'),
(FAILED, '失败'),
]

name = models.CharField(max_length=200)
status = models.IntegerField(choices=STATUS_CHOICES, default=PENDING)

# Django中字段默认是不允许为空,如果允许字段为空,通过null=True设置
start_time = models.DateField(null=True, default=None)
end_time = models.DateField(null=True, default=None)

# CharField:用于存储较短和固定长度的文本,必须指定 max_length
# TextField:用于存储任意长度的长文本,没有 max_length 限制
name = models.CharField(max_length=100)
item_info = models.TextField(default=None)

激活model:

创建app时会在app目录下的apps.py文件中生成如下代码:

1
2
3
4
5
from django.apps import AppConfig

class PollsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'polls'

创建polls app后需要将其安装到Django项目中,即将app配置PollsConfig添加到Django项目的settings.py文件中,这样就能激活model:

1
2
3
4
5
6
7
8
9
INSTALLED_APPS = [
"polls.apps.PollsConfig",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]

注意:写入INSTALLED_APPS中的值要和apps.py中name的值保持一致。

创建migration:

运行migration命令为model文件的更改创建migration,在此过程中,Django会将model文件的改动部分储存为一次migration:

1
2
3
4
5
>>> python3 manage.py makemigrations polls
Migrations for 'polls':
polls/migrations/0001_initial.py
- Create model Question
- Create model Choice

sqlmigrate 命令用于获取迁移名称并返回其 SQL,通过sql语句可以知晓此次迁移涉及的数据库操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
python3 manage.py sqlmigrate polls 0001
BEGIN;
--
-- Create model Question
--
CREATE TABLE "polls_question" (
"id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
"question_text" varchar(200) NOT NULL,
"pub_date" timestamp with time zone NOT NULL
);
--
-- Create model Choice
--
CREATE TABLE "polls_choice" (
"id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
"choice_text" varchar(200) NOT NULL,
"votes" integer NOT NULL,
"question_id" bigint NOT NULL
);
ALTER TABLE "polls_choice"
ADD CONSTRAINT "polls_choice_question_id_c5b4b260_fk_polls_question_id"
FOREIGN KEY ("question_id")
REFERENCES "polls_question" ("id")
DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id");

COMMIT;

执行migration:

运行 migrate 进行迁移,将更改应用到数据库,以在数据库中创建数据库表:

1
python manage.py migrate

数据库读写操作代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 获取Question表中的所有内容
q = Question.objects.all()
# 返回:<QuerySet [<Question: Question object (1)>]>

# 如果需要返回结果显示更准确详细的信息,可以在Question类中添加__str__()函数
# polls/models.py
class Question(models.Model):
# ...
def __str__(self):
return self.question_text
# 执行Question.objects.all()显示:
# <QuerySet [<Question: What's up?>]>

# 条件查询
q = Question.objects.filter(id=1)
q = Question.objects.filter(question_text__startswith="What")
q = Question.objects.get(pub_date__year=current_year)
q = Question.objects.get(id=1)
q = Question.objects.get(pk=1) # 通过主键查找

# 在Question类中还可以写其他函数作为过滤条件
class Question(models.Model):
# ...
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
# 执行查询
q = Question.objects.get(pk=1)
q.was_published_recently()

# 从数据库中获取primary key(主键)为1的Question实例
q = Question.objects.get(pk=1)
#查询与q这个Question实例相关联的所有Choice实例
# Django自动生成<model_name>_set属性用于关联反向查询
# 初始时返回空的QuerySet(查询集),即<QuerySet []>,表明这个问题还没有任何关联的选择(Choice)
q.choice_set.all()
#为q这个Question实例创建一个新的Choice实例,并设置choice_text为"Not much"和votes为0
# create方法会将新的Choice实例直接保存到数据库中
q.choice_set.create(choice_text="Not much", votes=0)
q.choice_set.create(choice_text="The sky", votes=0)
q.choice_set.all()
# 返回:<QuerySet [<Choice: Not much>, <Choice: The sky>]>
q.choice_set.count()
# 返回:2

Choice.objects.filter(question__pub_date__year=current_year)
# 返回:<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

# 删除数据
c = q.choice_set.filter(choice_text__startswith="Just hacking")
c.delete()

# 指定需要获取的字段f1, f2, f3
queryset = myModel.objects.filter(foo_icontains=bar).values('f1', 'f2', 'f3')

# 向Question表中插入/更新数据
q = Question(question_text="What's new?", pub_date=timezone.now())
q.save()

其他

创建python虚拟环境

1
python3 -m venv .

激活venv环境:

1
source bin/activate

退出venv环境:

1
2
cd bin/
deactivate

python项目生成requirements.txt:

1
pip3 freeze > requirements.txt

参考资料

https://docs.djangoproject.com/zh-hans/5.0/intro/

https://blog.lyh543.cn/posts/2020-12-27-django.html

进阶篇

Django REST Framework

背景知识

REST规范

REST(Representational State Transfer)是一种基于 HTTP 协议的 Web 架构风格,它大大简化了 Web 应用的开发和维护工作,成为现代 Web 开发的基础。REST 是一组架构规范,并非协议或标准,API 开发人员可以采用各种方式实现 REST。

Web API

Django是基于MVC开发模式的传统框架,非常适合开发基于PC的传统网站,因为它同时包括了后端的开发(逻辑层,数据库层) 和前端的开发(如模板语言,样式)。但是最近几年及未来几年更流行的开发模式是前后端分离, 现代网络应用(Web APP)或大型网站的设计一般是一个后端对应各种客户端(iOS, android, 浏览器),由于客户端的开发语言与后端的开发语言经常不同,所以需要后端能够提供一种可以跨平台跨语言的标准的资源或数据(如json格式)供前后端传输通信,Web API(网络应用程序接口)应运而生。可以把 API 看做是传递者,它是用户、客户端与资源、 Web 服务之间的中介。

RESTful API

RESTful API是遵循 REST 架构规范的API 或 Web API,支持与 RESTful Web 服务进行交互。下面介绍一些常见的 RESTful API 设计规范:

  1. RESTful API 中的操作应该使用 HTTP 动词来表达,例如 GET、POST、PUT、DELETE 等,以确保对资源的操作被明确表示和限制。如下所示:

    1
    2
    3
    4
    GET     /users/1
    POST /users
    PUT /users/1
    DELETE /users/1
  2. RESTful API 中应该使用名词来表示资源,而不是动词,以避免歧义和混淆。一般来说,数据库中的表都是同种记录的”集合”(collection),所以API中的名词也应该使用复数。例如:

    1
    2
    GET /users/1
    GET /orders/1

    如果需要对一个用户信息进行编辑或删除,传统Django开发可能将URL写成如下所示的形式:

    1
    2
    https://api.example.com/v1/users/{id}/edit/   # 编辑用户
    https://api.example.com/v1/users/{id}/delete/ # 删除用户

    其中API接口中的edit和delete都是动词,不符合规范,一个 URI应该是一个资源,本身不能包含任何动作,并且资源的URI地址应该是固定不变的,对同一资源应使用不同的HTTP请求方法来表示进行不同的操作,比如常见的增删查改。所以URL需要做如下修改:

    1
    2
    3
    4
    5
    [GET]       https://api.example.com/v1/users/1     // 查询
    [POST] https://api.example.com/v1/users // 新增/更新
    [PATCH] https://api.example.com/v1/users/1 // 更新
    [PUT] https://api.example.com/v1/users/1 // 覆盖,全部更新
    [DELETE] https://api.example.com/v1/users/1 // 删除
  3. RESTful API 应该使用 URI 来定位资源,以确保每个资源都有一个唯一的标识符。URI 应该具有层级结构,以便表示资源之间的关系。如果URL比较长,可能由多个单词组成,建议使用中划线-分隔,并且每个url的结尾不能加斜杠/。例如:

    1
    2
    3
    4
    5
    GET /users/1/orders/1

    https://api.example.com/v1/user-management/users/{id} # 推荐
    https://api.example.com/v1/user_management/users/{id} # 不推荐
    https://api.example.com/v1/user-management/users/{id}/ # 不推荐
  4. RESTful API 应该使用查询参数来过滤和分页资源,因为当查询到符合条件的记录数量非常多时,服务器一般不可能将所有的数据返回给用户。例如:

    1
    2
    3
    4
    5
    6
    7
    GET /users?gender=male
    GET /users?page=1&pageSize=10
    GET /users?limit=10
    GET /users?offset=10
    GET /users?page=2&per_page=100
    GET /users?sortby=name&order=asc
    GET /users?user_type_id=1
  5. RESTful API 应该使用 HTTP 状态码来表示请求结果,以便客户端能够根据状态码进行处理。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    200 OK - [GET]:服务器成功返回用户请求的数据
    201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功
    202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
    204 NO CONTENT - [DELETE]:用户删除数据成功
    400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作
    401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)
    403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的
    404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作
    406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)
    410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到
    422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误
    500 INTERNAL SERVER ERROR - [*]:服务器发生错误
  6. RESTful API 应该使用 JSON 或 XML 来表示数据,以便不同的客户端能够方便地进行数据解析和处理。例如:

    1
    2
    3
    4
    5
    6
    GET /users/1
    {
    "id": 1,
    "name": "Tom",
    "age": 25
    }
  7. RESTful API 应该使用版本号来管理 API 的不同版本,以便支持旧版 API 的兼容性和平稳升级。例如:

    1
    GET /v1/users/1
  8. RESTful API 应该使用 HATEOAS 来提高 API 的可发现性,HATEOAS(Hypermedia As The Engine Of Application State)是指使用超媒体作为应用程序状态的引擎,从而提高 RESTful API 的可发现性。通过使用 HATEOAS,客户端可以通过 API 返回的链接自主地遍历 API,并进行资源的操作。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    GET /users/1
    {
    "id": 1,
    "name": "Tom",
    "age": 25,
    "links": [
    {
    "rel": "orders",
    "href": "/users/1/orders"
    },
    {
    "rel": "edit",
    "href": "/users/1/edit"
    }
    ]
    }

    上述代码中的 links 字段包含了与当前资源相关的链接,客户端可以通过这些链接来访问其他资源。DRF(Django REST Framework)提供对HATEOAS的支持,实现此规范较简单。

Django REST Framework (DRF)

Django本身不支持符合REST规范的Web API, 但是可以借助Django REST Framework (DRF)快速开发出优秀并且符合规范的Web API。DRF是基于Django实现的一个RESTful API框架,能够快速开发RESTful风格的API,DRF官方文档中文版链接:https://q1mi.github.io/Django-REST-framework-documentation/ 。DRF可以使用pip安装:

1
python3 -m pip install djangorestframework

如果想要获取一个图形化的页面来操作API,需要将rest_framework注册到Django项目的INSTALLED_APPS中,如下所示:

1
2
3
4
5
6
7
8
9
10
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'your_app', # 自定义的app
]

序列化

每种编程语言都有各自的数据类型, 将属于自己语言的数据类型或对象转换为可通过网络传输或可以存储到本地磁盘的数据格式(如:XML、JSON或特定格式的字节串)的过程称为序列化(seralization);反之则称为反序列化。API开发的本质就是将各种后端语言不同的数据类型序列化为通用的可读可传输的数据格式,比如常见的json数据类型。

Django REST Framework的序列化工具,与django自带的serializers类相比,DRF的功能更强大,可以根据模型生成序列化器,还能对客户端发送过来的数据进行验证。

下面以博客系统项目为例,介绍DRF的常用组件。建议先读完DRF官方文档,再阅读下面的部分。

序列化器

开发Web API首先需要为其提供一种将数据/资源序列化和反序列化为如json之类的表示形式。

创建Article model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# blog/model.py
# 创建Article模型,用于存储博客的文章数据

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

User = get_user_model()

class Article(models.Model):
STATUS_CHOICES = (
('p', _('Published')),
('d', _('Draft')),
)

"""Article Model"""
title = models.CharField(max_length=100)
content = models.TextField(blank=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='articles')
status = models.CharField(max_length=1, choices=STATUS_CHOICES, default='d', null=True, blank=True)
create_date = models.DateTimeField(auto_now_add=True)

def __str__(self):
return self.title

class Meta:
ordering = ['-create_date']

上述代码创建Article模型(Article表),其中包括文章标题,内容,作者,状态和创建日期这些字段。接下来需要为Article模型创建初始迁移(initial migration),并首次同步数据库(migrate)。

1
2
python manage.py makemigrations snippets
python manage.py migrate

创建ArticleSerializer序列化器类

利用DRF开发Web API的第一步是自定义序列化器(serializers)。序列化器的作用是将模型实例(比如用户、文章)序列化和反序列化为诸如json之类的表示形式。一个模型实例可能有很多字段,但一般情况下不需要把所有字段数据都返回给用户。序列化器定义模型实例的哪些字段需要进行序列化/反序列化, 并且可以对客户端发送的数据进行验证和存储。

就像Django提供了Form类和ModelForm类两种方式自定义表单一样,REST framework提供了Serializer类和ModelSerializer类两种方式自定义序列化器。前者需手动指定需要序列化和反序列化的字段,后者根据模型(model)生成需要序列化和反序列化的字段,可以使代码更简洁。下面的代码是针对上述两种方式的示例:

使用Serializer类定义序列化器类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# blog/serializers.py

from rest_framework import serializers
from .models import Article
from django.contrib.auth import get_user_model

User = get_user_model()

class ArticleSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(required=True, max_length=100)
content = serializers.CharField(required=False, allow_blank=True)
author = serializers.ReadOnlyField(source="author.id")
status = serializers.ChoiceField(choices=Article.STATUS_CHOICES, default='d')
create_date = serializers.DateTimeField(read_only=True)

def create(self, validated_data):
"""
Create a new "Article" instance
"""
return Article.objects.create(**validated_data)

def update(self, instance, validated_data):
"""
Use validated data to update and return an existing Article instance
"""
instance.title = validated_data.get('title', instance.title)
instance.content = validated_data.get('content', instance.content)
instance.status = validated_data.get('status', instance.status)
instance.save()
return instance

序列化器类的第一部分定义了需要序列化/反序列化的字段。create()update()方法分别定义创建和更新文章的操作(在代码中调用serializer.save()创建和更新文章)。

使用ModelSerializer类定义序列化器类(推荐)

1
2
3
4
5
6
7
8
9
10
11
# blog/serializers.py

from rest_framework import serializers
from .models import Article

class ArticleSerializer(serializers.ModelSerializer):

class Meta:
model = Article
fields = '__all__'
read_only_fields = ('id', 'author', 'create_date')

read-only fields用于指定哪些字段不能由客户端通过POST或PUT请求提交相关的序列化数据进行修改。也可以直接指定序列化/反序列化的字段:

1
2
3
4
5
class SnippetSerializer(serializers.ModelSerializer):

class Meta:
model = Article
fields = ('title', 'content', 'author', 'create_date')

创建好ArticleSerializer序列化器类之后,就可以使用ArticleSerializer编写API视图(views)。DRF框架提供两个可用于编写API视图的包装器(wrappers):

  1. 用于基于函数视图(Functional Based View, FBV)的@api_view装饰器
  2. 用于基于类视图(Class Based View, CBV)的APIView

基于函数的API视图

编写两个基于函数的视图:article_list和article_detail,分别用于获取文章列表和文章详情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# blog/views.py

from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response

from .models import Article
from .serializers import ArticleSerializer

@api_view(['GET', 'POST'])
def article_list(request):

"""
List all articles, or create a new article.
"""
if request.method == 'GET':
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)

elif request.method == 'POST':
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
# Very important. Associate request.user with author
# ArticleSerializer序列化器中author字段是read-only,用户是无法通过POST提交自动修改
# 在创建Article实例时需手动将author和request.user绑定
serializer.save(author=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@api_view(['GET', 'PUT', 'DELETE'])
def article_detail(request, pk):

"""
Retrieve,update or delete an article instance
"""
try:
article = Article.objects.get(pk=pk)
except Article.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)

if request.method == 'GET':
serializer = ArticleSerializer(article)
return Response(serializer.data)

elif request.method == 'PUT':
serializer = ArticleSerializer(article, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

elif request.method == 'DELETE':
article.delete()
return Response(status=status.HTTP_204_NO_CONTENT)


DRF提供的@api_view装饰器实现了以下几大功能:

  • 与Django传统函数视图相区分,强调这是API视图,并限定了可以接受的请求方法
  • 拓展了Django原来的request对象,新的request对象不仅支持request.POST提交的数据,还支持其它请求方式如PUT或PATCH等方式提交的数据,所有的请求数据都保存在request.data字典里,方便Web API的开发
1
2
request.POST  # 只能处理表单数据	只适用于'POST'方法
request.data # 可以处理任意数据 适用于'POST','PUT'和'PATCH'方法

同时,代码不需要再显式地将请求或响应绑定到特定的内容类型比如HttpResponse和JSONResponse,统一使用Response方法返回响应,该方法支持内容协商,可根据客户端请求的内容类型返回不同的响应数据。

基于类的API视图(推荐)

一个中大型的Web项目代码量通常是非常大的,如果全部使用函数视图编写,那么代码的复用率是非常低的。使用类视图可以有效的提高代码复用,因为类是可以被继承和拓展的,特别是将一些可以共用的功能抽象成Mixin类或基类后可以减少重复造轮子的工作,使编写的代码符合”Don’t repeat yourself(DRY)”原则。

DRF推荐使用基于类的视图来开发API,并提供4种开发模式:

  • 使用基础APIView类
  • 使用Mixins类和GenericAPI类混合
  • 使用通用视图generics.*类,如 generics.ListCreateAPIView
  • 使用视图集ViewSets和路由器Routers

使用基础APIView类

DRF的APIView类继承自Django自带的View类, 一样可以按请求方法调用不同的处理函数,比如get方法处理GET请求,post方法处理POST请求。但是DRF的APIView功能更强大,它不仅支持更多的HTTP请求方法,而且还对Django的request对象进行了封装,可以使用request.data获取用户通过POST, PUT和PATCH方法发过来的数据,而且支持插拔式地配置认证、权限和限流类。

下面使用基础APIView类来重写之前的article_list和article_detail功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# blog/views.py
# 使用基础APIView类
from rest_framework.views import APIView
from django.http import Http404
from .models import Article
from .serializers import ArticleSerializer


class ArticleList(APIView):
"""
List all articles, or create a new article.
"""

def get(self, request, format=None):
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)

def post(self, request, format=None):
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
# 注意:手动将request.user与author绑定
serializer.save(author=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class ArticleDetail(APIView):
"""
Retrieve, update or delete an article instance.
"""

def get_object(self, pk):
try:
return Article.objects.get(pk=pk)
except Article.DoesNotExist:
raise Http404

def get(self, request, pk, format=None):
article = self.get_object(pk)
serializer = ArticleSerializer(article)
return Response(serializer.data)

def put(self, request, pk, format=None):
article = self.get_object(pk)
serializer = ArticleSerializer(instance=article, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

def delete(self, request, pk, format=None):
article = self.get_object(pk)
article.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

上述代码跟之前基于函数的视图差别并不大,最大的不同就是不需要再对用户的请求方法进行判断,视图可以自动将不同请求转发到相应处理方法,逻辑上也更清晰。

还需要修改项目的url配置, 让其指向新的基于类的视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# blog/urls.py

from django.urls import re_path
from rest_framework.urlpatterns import format_suffix_patterns

from . import views

urlpatterns = [
# re_path(r'^articles/$', views.article_list),
# re_path(r'^articles/(?P<pk>[0-9]+)$', views.article_detail),
re_path(r'^articles/$', views.ArticleList.as_view()),
re_path(r'^articles/(?P<pk>[0-9]+)$', views.ArticleDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

使用Mixins类和GenericAPI类混合

使用基于类视图的最大优势之一是它可以轻松地创建可复用的行为,但是使用基础的APIView类并没有大量简化代码,代码中的创建/获取/更新/删除操作和之前任何基于模型的API视图都非常相似。针于这些通用的增删改查行为,DRF已经在其提供的Mixin类中实现,并且Mixin类可以和generics.GenericAPI类混合使用,灵活组合成所需要的视图。

下面使用Mixins类和GenericAPI类混合的方式重写之前的article_list和article_detail功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 使用GENERIC APIView & Mixins
from rest_framework import mixins
from rest_framework import generics

class ArticleList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)

def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)

# 将request.user与author绑定
def perform_create(self, serializer):
serializer.save(author=self.request.user
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ArticleDetail(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)

def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)

def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)

在上述代码中,主要涉及GenericAPIView和CreateModelMixin、ListModelMixin、RetrieveModelMixin、UpdateModelMixin、DestroyModelMixin这几个类。

  1. GenericAPIView 类继承自APIView类,它不仅提供了基础的APIView类视图的能力,还可以通过queryset和serializer_class属性分别指定需要序列化与反序列化的model/queryset和所用到的序列化器类。下面是GenericAPIView类的部分源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    class GenericAPIView(views.APIView):
    """
    Base class for all other generic views.
    """
    # You'll need to either set these attributes,
    # or override `get_queryset()`/`get_serializer_class()`.
    # If you are overriding a view method, it is important that you call
    # `get_queryset()` instead of accessing the `queryset` property directly,
    # as `queryset` will get evaluated only once, and those results are cached
    # for all subsequent requests.
    queryset = None
    serializer_class = None

    # If you want to use object lookups other than pk, set 'lookup_field'.
    # For more complex lookup requirements override `get_object()`.
    lookup_field = 'pk'
    lookup_url_kwarg = None

    # The filter backend classes to use for queryset filtering
    filter_backends = api_settings.DEFAULT_FILTER_BACKENDS

    # The style to use for queryset pagination.
    pagination_class = api_settings.DEFAULT_PAGINATION_CLASS

    def get_queryset(self):
    """
    Get the list of items for this view.
    This must be an iterable, and may be a queryset.
    Defaults to using `self.queryset`.

    This method should always be used rather than accessing `self.queryset`
    directly, as `self.queryset` gets evaluated only once, and those results
    are cached for all subsequent requests.

    You may want to override this if you need to provide different
    querysets depending on the incoming request.

    (Eg. return a list of items that is specific to the user)
    """
    assert self.queryset is not None, (
    "'%s' should either include a `queryset` attribute, "
    "or override the `get_queryset()` method."
    % self.__class__.__name__
    )

    queryset = self.queryset
    if isinstance(queryset, QuerySet):
    # Ensure queryset is re-evaluated on each request.
    queryset = queryset.all()
    return queryset

    def get_object(self):
    """
    Returns the object the view is displaying.

    You may want to override this if you need to provide non-standard
    queryset lookups. Eg if objects are referenced using multiple
    keyword arguments in the url conf.
    """
    queryset = self.filter_queryset(self.get_queryset())

    # Perform the lookup filtering.
    lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

    assert lookup_url_kwarg in self.kwargs, (
    'Expected view %s to be called with a URL keyword argument '
    'named "%s". Fix your URL conf, or set the `.lookup_field` '
    'attribute on the view correctly.' %
    (self.__class__.__name__, lookup_url_kwarg)
    )

    filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
    obj = get_object_or_404(queryset, **filter_kwargs)

    # May raise a permission denied
    self.check_object_permissions(self.request, obj)

    return obj

    def get_serializer(self, *args, **kwargs):
    """
    Return the serializer instance that should be used for validating and
    deserializing input, and for serializing output.
    """
    serializer_class = self.get_serializer_class()
    kwargs.setdefault('context', self.get_serializer_context())
    return serializer_class(*args, **kwargs)

    def get_serializer_class(self):
    """
    Return the class to use for the serializer.
    Defaults to using `self.serializer_class`.

    You may want to override this if you need to provide different
    serializations depending on the incoming request.

    (Eg. admins get full serialization, others get basic serialization)
    """
    assert self.serializer_class is not None, (
    "'%s' should either include a `serializer_class` attribute, "
    "or override the `get_serializer_class()` method."
    % self.__class__.__name__
    )

    return self.serializer_class

    ···
  2. CreateModelMixin、ListModelMixin、RetrieveModelMixin、UpdateModelMixin和DestroyModelMixin这几个类分别引入.create()、.list()、.retrieve()、.update()和.destroy()方法。mixin类引入的方法以create、list、retrieve、update和destroy命名,而不是继续使用现有的get、post、put和delete等方法,是因为请求方法不如操作名字清晰,比如get方法同时对应获取对象列表和单个对象这两种操作,使用list和retrieve命名更容易区分,list()方法用于获取对象列表,retrieve()方法用于获取单个对象;另外post方法接收用户发送的请求数据后,有些情况下只需要转发而不需要创建model对象实例,所以post方法不能简单的等同于create方法。在使用mixin类提供的.list()、.create()等操作时,只需明确地将get和post等方法绑定到适当的操作即可。下面是CreateModelMixin、ListModelMixin、RetrieveModelMixin、UpdateModelMixin和DestroyModelMixin这几个类的源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    # python3.9/site-packages/rest_framework/mixins.py
    class CreateModelMixin:
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)
    return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
    serializer.save()

    def get_success_headers(self, data):
    try:
    return {'Location': str(data[api_settings.URL_FIELD_NAME])}
    except (TypeError, KeyError):
    return {}


    class ListModelMixin:
    """
    List a queryset.
    """
    def list(self, request, *args, **kwargs):
    queryset = self.filter_queryset(self.get_queryset())

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

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


    class RetrieveModelMixin:
    """
    Retrieve a model instance.
    """
    def retrieve(self, request, *args, **kwargs):
    instance = self.get_object()
    serializer = self.get_serializer(instance)
    return Response(serializer.data)


    class UpdateModelMixin:
    """
    Update a model instance.
    """
    def update(self, request, *args, **kwargs):
    partial = kwargs.pop('partial', False)
    instance = self.get_object()
    serializer = self.get_serializer(instance, data=request.data, partial=partial)
    serializer.is_valid(raise_exception=True)
    self.perform_update(serializer)

    if getattr(instance, '_prefetched_objects_cache', None):
    # If 'prefetch_related' has been applied to a queryset, we need to
    # forcibly invalidate the prefetch cache on the instance.
    instance._prefetched_objects_cache = {}

    return Response(serializer.data)

    def perform_update(self, serializer):
    serializer.save()

    def partial_update(self, request, *args, **kwargs):
    kwargs['partial'] = True
    return self.update(request, *args, **kwargs)


    class DestroyModelMixin:
    """
    Destroy a model instance.
    """
    def destroy(self, request, *args, **kwargs):
    instance = self.get_object()
    self.perform_destroy(instance)
    return Response(status=status.HTTP_204_NO_CONTENT)

    def perform_destroy(self, instance):
    instance.delete()

在新的ArticleList视图类中,定义的序列化器ArticleSeralizer类并不包含author字段,需要在创建article实例时将author与request.user进行手动绑定。在前面的例子中使用serializer.save(author=request.user)这一方法进行手动绑定,在使用mixin类之后,需要重写perform_create方法进行手动绑定。.perform_create这个钩子函数是CreateModelMixin类自带的,用于执行创建对象时需要执行的其它方法,比如发送邮件等功能,有点类似于Django的信号。类似的钩子函数还有UpdateModelMixin提供的.perform_update方法和DestroyModelMixin提供的.perform_destroy方法。

使用通用视图generics.*类

前面通过使用mixin类,减少了编写视图的代码量,但是将get请求与mixin提供的list方法进行绑定还是有些许冗余,为此DRF框架提供了一组已经将 Mixin 类与 GenericAPI 类混合好的视图,开箱即用,可以进一步简化视图的代码。

下面使用通用视图generics.*类来重写之前的ArticleList和ArticleDetail功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用通用视图generics.*类
from rest_framework import generics

class ArticleList(generics.ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

# 将request.user与author绑定
def perform_create(self, serializer):
serializer.save(author=self.request.user)

class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Article.objects.all()
serializer_class =ArticleSerializer

generics.ListCreateAPIView类支持List、Create两种视图功能,分别对应GET和POST请求;generics.RetrieveUpdateDestroyAPIView支持Retrieve、Update、Destroy操作,分别对应方法分别是GET、PUT和DELETE。其它常用generics.*类视图还包括ListAPIView, RetrieveAPIView, RetrieveUpdateAPIView等。

下面是generics.*类的部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
"""
Generic views that provide commonly needed behaviour.
"""
from django.core.exceptions import ValidationError
from django.db.models.query import QuerySet
from django.http import Http404
from django.shortcuts import get_object_or_404 as _get_object_or_404

from rest_framework import mixins, views
from rest_framework.settings import api_settings

···
class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
"""
# You'll need to either set these attributes,
# or override `get_queryset()`/`get_serializer_class()`.
# If you are overriding a view method, it is important that you call
# `get_queryset()` instead of accessing the `queryset` property directly,
# as `queryset` will get evaluated only once, and those results are cached
# for all subsequent requests.
queryset = None
serializer_class = None

# If you want to use object lookups other than pk, set 'lookup_field'.
# For more complex lookup requirements override `get_object()`.
lookup_field = 'pk'
lookup_url_kwarg = None

# The filter backend classes to use for queryset filtering
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS

# The style to use for queryset pagination.
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS

def get_queryset(self):
"""
Get the list of items for this view.
This must be an iterable, and may be a queryset.
Defaults to using `self.queryset`.

This method should always be used rather than accessing `self.queryset`
directly, as `self.queryset` gets evaluated only once, and those results
are cached for all subsequent requests.

You may want to override this if you need to provide different
querysets depending on the incoming request.

(Eg. return a list of items that is specific to the user)
"""
assert self.queryset is not None, (
"'%s' should either include a `queryset` attribute, "
"or override the `get_queryset()` method."
% self.__class__.__name__
)

queryset = self.queryset
if isinstance(queryset, QuerySet):
# Ensure queryset is re-evaluated on each request.
queryset = queryset.all()
return queryset

def get_object(self):
"""
Returns the object the view is displaying.

You may want to override this if you need to provide non-standard
queryset lookups. Eg if objects are referenced using multiple
keyword arguments in the url conf.
"""
queryset = self.filter_queryset(self.get_queryset())

# Perform the lookup filtering.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

assert lookup_url_kwarg in self.kwargs, (
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwarg)
)

filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)

# May raise a permission denied
self.check_object_permissions(self.request, obj)

return obj

def get_serializer(self, *args, **kwargs):
···


# Concrete view classes that provide method handlers
# by composing the mixin classes with the base view.

class CreateAPIView(mixins.CreateModelMixin,
GenericAPIView):
"""
Concrete view for creating a model instance.
"""
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)


class ListAPIView(mixins.ListModelMixin,
GenericAPIView):
"""
Concrete view for listing a queryset.
"""
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)


class RetrieveAPIView(mixins.RetrieveModelMixin,
GenericAPIView):
"""
Concrete view for retrieving a model instance.
"""
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)


class DestroyAPIView(mixins.DestroyModelMixin,
GenericAPIView):
"""
Concrete view for deleting a model instance.
"""
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)


class UpdateAPIView(mixins.UpdateModelMixin,
GenericAPIView):
"""
Concrete view for updating a model instance.
"""
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)

def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)


class ListCreateAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin,
GenericAPIView):
"""
Concrete view for listing a queryset or creating a model instance.
"""
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)

def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)


class RetrieveUpdateAPIView(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
GenericAPIView):
"""
Concrete view for retrieving, updating a model instance.
"""
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)

def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)

def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)


class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
GenericAPIView):
"""
Concrete view for retrieving or deleting a model instance.
"""
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)

def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)


class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
GenericAPIView):
"""
Concrete view for retrieving, updating or deleting a model instance.
"""
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)

def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)

def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)

def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)

使用视图集ViewSets

使用通用视图generics.*类后视图的代码已经大大简化,但是ArticleList和ArticleDetail两个类中queryset和serializer_class属性依然存在代码重复。使用视图集可以将两个类视图进一步合并,一次性提供List、Create、Retrieve、Update、Destroy这5种常见操作,这样queryset和seralizer_class属性也只需要定义一次, 如下所示:

1
2
3
4
5
6
7
8
9
10
# blog/views.py
from rest_framework import viewsets

class ArticleViewSet(viewsets.ModelViewSet):
# 用一个视图集替代ArticleList和ArticleDetail两个视图
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 自行添加,将request.user与author绑定
def perform_create(self, serializer):
serializer.save(author=self.request.user)

使用视图集开发API还需要使用DRF提供的路由器Routers来分发urls,同时也可以显式地将ViewSets绑定到url;因为一个视图集对应多个urls,而不像之前一个url对应一个视图函数或一个视图类。

  1. 使用路由器Routers分发urls:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # blog/urls.py
    from django.urls import re_path
    from rest_framework.urlpatterns import format_suffix_patterns
    from . import views
    from rest_framework.routers import DefaultRouter

    # 创建路由器并注册视图集
    router = DefaultRouter()
    router.register(r'articles', viewset=views.ArticleViewSet)

    # API URL现在由路由器自动确定
    urlpatterns = [
    # re_path(r'^articles/$', views.ArticleList.as_view()),
    # re_path(r'^articles/(?P<pk>[0-9]+)$', views.ArticleDetail.as_view()),
    ]
    # urlpatterns = format_suffix_patterns(urlpatterns)

    urlpatterns += router.urls

    使用ViewSet类实际上不需要自己设计URL,将资源连接到视图和url的约定由Router类自动处理,在代码中只需要使用路由器注册相应的视图集,然后让它执行其余操作即可;

  2. 显式地将ViewSets绑定到urls

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # blog/urls.py
    from django.urls import re_path
    from rest_framework.urlpatterns import format_suffix_patterns
    from . import views
    # from rest_framework.routers import DefaultRouter
    # router = DefaultRouter()
    # router.register(r'articles', viewset=views.ArticleViewSet)
    article_list = views.ArticleViewSet.as_view(
    {
    'get': 'list',
    'post': 'create'
    })

    article_detail = views.ArticleViewSet.as_view({
    'get': 'retrieve', # 只处理get请求,获取单个对象
    })

    urlpatterns = [
    re_path(r'^articles/$', article_list),
    re_path(r'^articles/(?P<pk>[0-9]+)$', article_detail),
    ]

    urlpatterns = format_suffix_patterns(urlpatterns)

    有些情况下只需要一个视图集对应的List、Create、Retrieve、Update、Destroy这5种操作中的几种操作,可以通过在urls.py中显式指定方法映射来实现。

    另外DRF还提供了viewsets.ReadOnlyModelViewSet 类,仅支持list和retrive操作。

视图(views) vs 视图集(viewsets)

视图集是一个非常有用的抽象,它有助于确保URL约定在API中保持一致,最大限度地减少编写所需的代码量,让开发者更专注于API提供的交互和表示,而不是URLconf的细节。但是这并不意味着采用视图集总是正确的方法,使用视图集不像单独构建视图那样明确。

CBV类对比

开发模式 优势 适用场景
基础APIView类 可读性最高,代码最多,灵活性最高 适用于需要对的API行为进行个性化定制的场景
Mixins类和GenericAPI类混合 同“通用视图generics.*类” 同“通用视图generics.*类”
通用视图generics.*类 可读性高,代码适中,灵活性较高 适用于需要对一个model进行标准的增删查改操作中的全部或部分操作的场景
视图集ViewSets 可读性较低,代码最少,灵活性最低 适用于需要对一个模型进行标准的增删查改操作中的全部操作且不需要定制API行为的场景

分页(Pagination)

当数据库中的数据量非常大时,如果一次将所有数据查询出来,会大大增加服务器内存的负载,降低系统的运行速度。一种更好的方式是将数据分段展示给用户,如果用户在展示的分段数据中没有找到自己的内容,可以通过指定页码或翻页的方式查看更多数据,直到找到自己想要的内容为止。

Django REST Framework提供3种分页类:

  1. PageNumberPagination类:简单分页器,支持用户按?page=3这种方式进行查询,在代码中可以通过page_size参数手动指定每个page展示给用户的数据条数,同时它还支持用户按?page=3&size=10这种更灵活的方式进行查询,这样不仅可以选择页码,还可以选择每页展示的数据条数。对于第二种情况,通常还需要在代码中设置max_page_size参数限制每页展示数据的最大数量,以防止用户进行恶意查询(比如size=10000);
  2. LimitOffsetPagination类:偏移分页器,支持用户按?limit=20&offset=100这种方式进行查询,offset是查询数据的起始点,limit是每页展示数据的最大条数,类似于page_size。使用这个类时,通常还需要设置max_limit参数来限制展示给用户的数据的最大条数;
  3. CursorPagination类:加密分页器,这是DRF提供的加密分页查询,仅支持用户按响应提供的上一页和下一页链接进行分页查询,每页的页码都是加密的。使用这种方式进行分页需要模型有”created”创建时间字段,否则需要手动指定ordering排序才能进行使用。

使用PageNumberPagination类

如果希望用户按?page=3&size=10这种更灵活的方式进行查询,就要在代码中进行个性化定制。在实际开发过程中,定制比使用默认的分页类更常见。

为了创建自定义分页序列化程序类,应将 pagination.BasePagination 子类化,按需要重写paginate_queryset()get_paginated_response()方法:

  • paginate_queryset(self, queryset, request, view=None) 方法接收初始的queryset和请求数据,并返回一个iterable对象,该对象只包含请求页中的数据
  • get_paginated_response(self, data) 方法接收序列化的页数据,并应返回一个 Response

首先在app目录下新建pagination.py,添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#blog/pagination.py
from rest_framework.pagination import PageNumberPagination

class MyPageNumberPagination(PageNumberPagination):
page_size = 2 # default page size
page_size_query_param = 'size' # ?page=xx&size=xx
max_page_size = 100 # max page size

def get_paginated_response(self, data):
return Response({
'links': {
'next': self.get_next_link(), # 下一页链接
'previous': self.get_previous_link() # 上一页链接
},
'count': self.page.paginator.count, # 所有分页的数据总数
'results': data # 单页的数据
})

上述代码自定义了一个MyPageNumberPagination类,该类继承自PageNumberPagination类,通过page_size设置每页默认展示的数据条数,通过page_size_query_param设置用户查询时自定义每页数据条数的参数名size,通过max_page_size设置单页可以展示的最大数据条数。定制分页类还可以通过重写get_paginated_response方法改变响应数据的输出格式,比如把next和previous放在一个名为links的key里。

在函数或简单的APIView视图中使用PageNumberPagination类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from .pagination import MyPageNumberPagination

class ArticleList(APIView):
"""
List all articles, or create a new article.
"""
def get(self, request, format=None):
articles = Article.objects.all()

page = MyPageNumberPagination()

ret = page.paginate_queryset(articles, request)
serializer = ArticleSerializer(ret, many=True)

return Response(serializer.data)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class VehicleList(APIView):

···
def get(self, request: Request):
ordering=request.query_params.get("ordering","-id")
queryset_list = Vehicle.objects.all().order_by(ordering)

# 分页

page = CustomPageNumberPagination()
# 调用paginate_queryset返回分页后的单页数据
ret = page.paginate_queryset(queryset_list, request)
serializer = VehicleSerializer(ret, many=True)
data = serializer.data
···

*在generics.APIView和视图集ViewSets视图中使用PageNumberPagination类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from rest_framework import viewsets
from .pagination import MyPageNumberPagination


class ArticleViewSet(viewsets.ModelViewSet):
# 用一个视图集替代ArticleList和ArticleDetail两个视图
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 在视图集中可以使用pagination_class属性指定自定义的分页类
pagination_class = MyPageNumberPagination

# 自行添加,将request.user与author绑定
def perform_create(self, serializer):
serializer.save(author=self.request.user)

# 自行添加,将request.user与author绑定
def perform_update(self, serializer):
serializer.save(author=self.request.user)

注意:pagination_class属性仅支持在genericsAPIView和视图集viewset中配置使用。

使用LimitOffsetPagination类

1
2
3
4
5
6
7
8
9
#blog/pagination.py
from rest_framework.pagination import LimitOffsetPagination


class MyLimitOffsetPagination(LimitOffsetPagination):
default_limit = 5 # default limit per age
limit_query_param = 'limit' # default is limit
offset_query_param = 'offset' # default param is offset
max_limit = 10 # max limit per age

使用CursorPagination类

1
2
3
4
5
6
7
8
9
#blog/pagination.py
from rest_framework.pagination import CursorPagination


class MyArticleCursorPagination(CursorPagination):
page_size = 3 # Default number of records per age
page_size_query_param = 'page_size'
cursor_query_param = 'cursor' # Default is cursor
ordering = '-create_date'

CursorPagination类只能对排过序的查询集进行分页展示,所以使用CursorPagination类时需要在模型中定义created字段,否则需要手动指定ordering字段。Article模型中没有定义created字段,需要手动指定按create_date字段排序。

参考资料

https://q1mi.github.io/Django-REST-framework-documentation/

https://www.cnblogs.com/LLBFWH/category/2028316.html

https://blog.lyh543.cn/posts/2021-01-24-django-rest-framework.html

drf-yasg