最近在使用 Django
配合 DRF
(django-rest-framework
) 开发 CD
系统调度器,遇到了这个需求。
之前做侧开,主要侧重于测试,也进行过这种列表接口的测试。也熟悉了标准的调用方法。现在主要侧重于开发,就轮到我开发这种接口了。
查了下 DRF
默认支持的查询机制和前端需要的有点不一样。所以在这里把具体实现记录下。
手头上现有一个发布单的列表接口,可以返回目前已经创建的发布单信息,并结合 DRF
已经配置了序列化和分页。
现在需要接口可以支持使用请求参数来过滤返回的发布单列表信息,也就是添加查询参数。并且要支持某个参数的多选查询。
实际请求过来的 url
会在原先的 url
后添加上类似 ?name=first&status=0,1
这样的一串信息。它的意思是:使用 name
和 status
来进行查询,返回 (name
为 first
) 且 (status
为 0
或 1
)
DRF
中提供了一个组件 SearchFilter
(源码在此)来提供查询功能。该组件可以通过类属性 search_param
指定 url
中的查询关键字,通过类属性 search_fields
指定 查询关键字对应的数据表的字段。一个关键字可以对应多个字段。
SearchFilter
默认的匹配逻辑是:只要数据表中某条记录的某个指定字段满足匹配,就记为当前查询关键字的一条查询结果。
举个例子,假设现在的情况是:
name
,first
,icontains
,id
、pre_name
和 post_name
,search_fields
设置为关联 pre_name
和 post_name
。则最终返回的查询结果是 pre_name
或 post_name
字段的值包含 first
的记录的集合
SearchFilter
查询关键字对应的多个 数据表字段之间采用了 或逻辑。DRF
中的一个 ListCreateAPIView
可以支持设定多个 SearchFilter
,以列表的形式使用。每个 SearchFilter
可以定制对应的 search_param
和 search_fields
,多个 SearchFilter
的结果之间采用 与逻辑
SearchFilter
对于多选查询的默认逻辑是 与逻辑。
举个例子,假设现在的情况是:
status
,0,1
,iexact
,id
、name
和 status
,search_fields
设置为关联 status
。则最终返回的查询结果是 status
字段的值 等于 0
且 等于 1
的记录的集合。
这不是扯淡嘛。需求需要的是返回 status
字段的值 等于 0
或 等于 1
的记录的集合。
这里的难点就在于 SearchFilter
多选查询的逻辑被硬编码到代码中,预先并没有给出定制的地方。总不能直接覆盖对应的方法吧。虽然也不失为一种办法。
解决思路的话,我认为是有三个的。分别是:
构造 SQL
自己定制查询。
这里尽量使用 Django
objects
提供的 raw
方法,官方教程点我,它内置了对 SQL
注入的防范。
但我是 ORM
的仔,所以此方案 PASS
。
重写 filter_queryset
方法。
filter_queryset
被 SearchFilter
用来过滤查询集,返回最终查询结果。 重写 filter_queryset
其实就是继承 SearchFilter
,把下图红框部分改成 operator.or_
。
为了实现或逻辑,在一坨代码中修改这么个小地方。如果下个版本在这坨代码中出个 Bug
,官方修正后,你是跟着改还是不改呢。为了避免这个选择,此方案依然 PASS
。
重写 get_search_terms
方法。
get_search_terms
用于提取请求 url
中查询关键字对应的值。以逗号分隔,每个值是一个 term
。
例如,url
为 ***?status=1,2
,1
和 2
都是一个 term
。get_search_terms
返回 term
列表。从上图的 116-122 行可以看到每个 term
内部是采用 或逻辑,term
之间是采用 与逻辑。
直接将 与逻辑 改成 或逻辑 就是刚刚说的第二个方法。
而在 term
列表外再加个列表,并把数据表匹配逻辑改成 in
,就是本方法。这样就不存在 term
之间的 与逻辑。SearchFilter
默认没有给出 in
的匹配方式,这里就入乡随俗,用正则匹配 iregex
。此方案 ACCEPTED
。
这个很简单,我直接贴代码了。实现的功能是:
name
,icontains
,pre_name
和 post_name
字段。下列三个文件处于同一个文件夹中。
models.py
class Release(models.Model):
pre_name = models.CharField(max_length=30)
post_name = models.CharField(max_length=30)
filters.py
# coding:utf-8
from rest_framework.filters import SearchFilter
class NameSearchFilter(SearchFilter):
search_param = 'name'
def get_search_fields(self, view, request):
return ['pre_name', 'post_name']
apis.py
from rest_framework import generics
from .serializers import ReleaseSerializer
from .models import Release
from .filters import NameSearchFilter
class List(generics.ListAPIView):
queryset = Release.objects.all()
serializer_class = ReleaseSerializer
filter_backends = [NameSearchFilter]
多选查询实现的功能是:
status
,iregex
,status
字段。下列三个文件处于同一个文件夹中。
models.py
class Release(models.Model):
name = models.CharField(max_length=30)
status = models.IntegerField(default=0)
filters.py
# coding:utf-8
from rest_framework.filters import SearchFilter
class StatusSearchFilter(SearchFilter):
search_param = 'status'
def get_search_fields(self, view, request):
# $ 代表使用 iregex 模式进行字段匹配
return ['$status']
def get_search_terms(self, request):
# 获取默认返回的 terms 列表
terms = super().get_search_terms(request):
return [terms]
apis.py
from rest_framework import generics
from .serializers import ReleaseSerializer
from .models import Release
from .filters import StatusSearchFilter
class List(generics.ListAPIView):
queryset = Release.objects.all()
serializer_class = ReleaseSerializer
filter_backends = [StatusSearchFilter]
如果要定制多个查询参数,只要在 filter_backends
列表中添加对应的 SearchFilter
。