作者:HelloGitHub-追梦人物[1]
文中所涉及的示例代码,已同步更新到 HelloGitHub-Team 仓库[2]
我们的博客有一个侧边栏功能,分别列出博客文章的分类列表、标签列表、归档时间列表,通过点击侧边栏对应的条目,还可以进入相应的页面。例如点击某个分类,博客将跳转到该分类下全部文章列表页面。这些数据的展示都需要开发对应的接口,以便前端调用获取数据。
分类列表、标签列表实现比较简单,我们这里给出接口的设计规范,大家可以使用前几篇教程中学到的知识点轻松实现(具体实现可参考 GtiHub 上的源代码)。
分类列表接口:/categories/
标签列表接口:/tags/
归档日期列表的接口实现稍微复杂一点,因为我们需要从已有文章中归纳文章发表日期。事实上,我们在上一部教程 HelloDjango - Django博客教程(第二版)的 页面侧边栏:使用自定义模板标签 已经讲解了如何获取归档日期列表,只是当时返回的归档日期列表直接用于模板的渲染,而这里我们需要将归档日期列表序列化后通过 API 接口返回。
具体来说,获取博客文章发表时间归档列表的方法是调用查询集(QuerySet)的 dates
方法,提取记录中的日期。核心代码就一句:
Post.objects.dates('created_time', 'month', order='DESC')
这里 Post.objects.dates
方法会返回一个列表,列表中的元素为每一篇文章(Post)的创建日期(已去重),日期都是 Python 的 date
对象,精确到月份,降序排列。
有了返回的归档日期列表,接下来就实现相应的 API 接口视图函数:
代码语言:javascript复制blog/views.py
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.serializers import DateField
class PostViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
# ...
@action(
methods=["GET"], detail=False, url_path="archive/dates", url_name="archive-date"
)
def list_archive_dates(self, request, *args, **kwargs):
dates = Post.objects.dates("created_time", "month", order="DESC")
date_field = DateField()
data = [date_field.to_representation(date) for date in dates]
return Response(data=data, status=status.HTTP_200_OK)
注意这里我们涉及到了几个以前没有详细讲解过的用法。
一是 action
装饰器,它用来装饰一个视图集中的方法,被装饰的方法会被 django-rest-framework 的路由自动注册为一个 API 接口。
回顾一下我们之前在使用视图集 viewset 时提到过 action(动作)的概念,django-rest-framework 预定义了几个标准的动作,分别为 list 获取资源列表,retrieve 获取单个资源、update 和 partial_update 更新资源、destroy 删除资源,这些 action 具体的实现方法,分别由 mixins 模块中的混入类提供。例如 用类视图实现首页 API 中我们介绍过 mixins.ListModelMixin
,这个混入类提供了 list 动作对应的标准实现,即 list 方法。视图集中所有以上提及的以标准动作命名的方法,都会被 django-rest-framework 的路由自动注册为标准的 API 接口。
django-rest-framework 默认只能识别标准命名的视图集方法并将其注册为 API,但我们可以添加更多非标准的 action,而为了让 django-rest-framework 能够识别这些方法,就需要使用 action
装饰器进行装饰。
其实我们可以简单地将 action 装饰的方法看作是一个视图函数的实现,因此可以看到方法传入的第一个参数为 request 请求对象,函数体就是这个视图函数需要执行的逻辑,显然,方法最终必须要返回一个 HTTP 响应对象。
action 装饰器通常用于在视图集中添加额外的接口实现。例如这里我们已有了 PostViewSet
视图集,标准的 list 实现了获取文章资源列表的逻辑。我们想添加一个获取文章归档日期列表的接口,因此添加了一个 list_archive_dates
方法,并使用 action 进行装饰。通常如果要在视图集中添加额外的接口实现,可以使用如下的模板代码:
@action(
methods=["allowed http method name"],
detail=False or True,
url_path="url/path",
url_name="url name"
)
def method_name(self, request, *args, **kwargs):
# 接口逻辑的具体实现,返回一个 Response
通常 action 装饰器以下 4 个参数都会设置:
methods:一个列表,指定访问这个接口时允许的 HTTP 方法(GET、POST、PUT、PATCH、DELETE)
detail:True 或者 False。设置为 True,自动注册的接口 URL 中会添加一个 pk 路径参数(请看下面的示例),否则不会。
url_path:自动注册的接口 URL。
url_name:接口名,主要用于通过接口名字反解对应的 URL。
当然,我们还可以在 action 中设置所有 ViewSet
类所支持的类属性,例如 serializer_class
、pagination_class
、permission_classes
等,用于覆盖类视图中设置的属性值。
以上是 action 用法的一个基本介绍,现在来分析一下 list_archive_dates
这个 action 来加深理解。
methods
参数指定接口需要通过 GET 方法访问,detail 为 False
,url_path
设置为 archive/dates,因此最终自动生成的接口路由就是 /posts/archive/dates/。如果我们设置 detail 为 True,那么生成的接口路由就是 /posts/<int:pk>/archive/dates/
,生成的 URL 中就会多一个 pk 路径参数。
list_archive_dates
具体的实现逻辑中,以下几点需要注意:
一是独立使用序列化字段(Field)。之前序列化字段都是在序列化器(Serializer)里面使用的,因为通常来说接口需要序列化一个对象的多个字段。而这个接口中只需要序列化一个时间字段(类型为 Python 标准库中的 datetime.date
),所以没必要单独定义一个序列化器了,直接拿 django-rest-framework 提供的用于序列化时间类型的 DateField
就可以了。用法也很简单,实例化序列化字段,调用其 to_representation
方法,将需要序列化的值传入即可(其实序列化器在序列对象的多个字段时,内部也是分别调用对应序列化字段的 to_representation
方法)。
我们通过列表推导式生成一个序列化后的归档日期列表,这个列表是可被序列化的。接着我们在接口返回一个 Response
, Response
将序列化后的结果包装返回(保存在 data 属性中),django-rest-framework 会进一步帮我们把这个 Response
中包含的数据解析为合适的格式(例如 JSON)。
status=status.HTTP_200_OK
指定这个接口返回的状态码,HTTP_200_OK
是一个预定义的常数,即 200。django-rest-framework 将常用 HTTP 请求的状态码常数预定义 status 模块里,使用预定义的变量而不是直接使用数字的好处一是增强代码可读性,二是减少硬编码。
由于 PostViewSet
视图集已经通过 django-rest-framework 的路由进行了注册,因此 list_archive_dates
也会被连带着自动注册为一个接口。启动开发服务器,访问 /posts/archive/dates/,就可以看到返回的文章归档日期列表。
注意到红框圈出部分,django-rest-framework API 交互后台会识别到额外定义的 action 并将它们展示出来,点击就可以进入到相应的 API 页面。
现在,侧边栏所需要的数据接口就开发完成了,接下来实现返回某一分类、标签或者归档日期下的文章列表接口。
在 使用视图集简化代码 我们开发了获取全部文章的接口。事实上,分类、标签或者归档日期文章列表的 API,本质上还是返回一个文章列表资源,只不过比首页 API 返回的文章列表资源多了个“过滤”,只过滤出了指定的部分文章而已。对于这样的场景,我们可以在请求 API 时加上查询参数,django-rest-framework 解析查询参数,然后从全部文章列表中过滤出查询所指定的文章列表再返回。
这在 RESTful API 的设计中肯定是会遇到的,因此第三方库 django-filter 帮我们实现了上述所说的查询过滤功能,而且和 django-rest-framework 有很好的集成,我们可以在 django-rest-framework 中非常方便地使用 django-filter。
既然要使用它,当然是先安装它(已安装跳过):pipenv install django-filter
接着我们来配置 PostViewSet
,为其设置用于过滤返回结果集的一些属性,代码如下:
from django_filters.rest_framework import DjangoFilterBackend
from .filters import PostFilter
class PostViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
# ...
filter_backends = [DjangoFilterBackend]
filterset_class = PostFilter
非常的简单,仅仅设置了 filter_backends
和 filterset_class
两个属性。其中 filter_backends
设置为 DjangoFilterBackend
,这样 API 在返回结果时, django-rest-framework 会调用设置的 backend(这里是 DjangoFilterBackend
) 的 filter
方法对 get_queryset
方法返回的结果进行进一步的过滤,而 DjangoFilterBackend
会依据 filterset_class
(这里是 PostFilter
)中定义的过滤规则来过滤查询结果集。
当然 PostFilter
还没有定义,我们来定义它。首先在 blog 应用下创建一个 filters.py 文件,用于存放自定义 filter 的代码,PostFilter
代码如下:
from django_filters import rest_framework as drf_filters
from .models import Post
class PostFilter(drf_filters.FilterSet):
created_year = drf_filters.NumberFilter(
field_name="created_time", lookup_expr="year"
)
created_month = drf_filters.NumberFilter(
field_name="created_time", lookup_expr="month"
)
class Meta:
model = Post
fields = ["category", "tags", "created_year", "created_month"]
PostFilter
的定义和序列化器 Serializer 非常类似。
category
,tags
两个过滤字段因为是 Post
模型中定义的字段,因此 django-filter 可以自动推断其过滤规则,只需要在 Meta.fields
中声明即可。
归档日期下的文章列表,我们设计的接口传递 2 个查询参数:年份和月份。由于这两个字段在 Post
中没有定义,Post
记录时间的字段为 created_time
,因此我们需要显示地定义查询规则,定义的规则是:
查询参数名 = 查询参数值的类型(查询的模型字段,查询表达式)
例如示例中定义的 created_year
查询参数,查询参数值的类型为 number,即数字,查询的模型字段为 created_time
,查询表达式是 year
。当用户传递 created_year
查询参数时,django-filter 实际上会将以上定义的规则翻译为如下的 ORM 查询语句:
Post.objects.filter(created_time__year=created_year传递的值)
现在回到 API 交互后台,先进到 /post/ 接口下,默认返回了全部文章列表。可以看到右上角多了个过滤器(红框圈出部分)。
点击会弹出过滤参数输入的交互面板,在这里可以交互式地输入查询过滤参数的值。
例如选择如下的过滤参数,得到查询的 URL 为:
http://127.0.0.1:10000/api/posts/?category=1&tags=1&created_year=2020&created_month=1
这条查询返回创建于 2020 年 1 月,id 为 1 的分类下,id 为 1 的标签下的全部文章。
通过不同的查询参数组合,就可以得到不同的文章资源列表了。
参考资料
[1]
HelloGitHub-追梦人物: https://www.zmrenwu.com
[2]
HelloGitHub-Team 仓库:https://github.com/HelloGitHub-Team/HelloDjango-REST-framework-tutorial