手摸手,带你用Django REST Framework撸接口系列九(过滤篇)

REST framework列表视图的默认行为是返回一个model的全部queryset。通常你却想要你的API来限制queryset返回的数据。这个时候就引入了我们的过滤器。

项目需求

当你发送GET请求到/api/articles?page=3时可以得到下面返回的分页数据列表。现在我们希望对结果进行进一步过滤,比如返回标题含有关键词”华为”的文章资源列表。我们到底该怎么做呢? 本例中演示三种过滤方法, 你可以根据实际项目开发需求去使用。

重写get_queryset

最简单的过滤任意GenericAPIView子视图queryset的方法就是重写它的.get_queryset()方法。

重写这个方法允许你使用很多不同的方式来定制视图返回的queryset。

此方法不依赖于任何第三方包, 只适合于需要过滤的字段比较少的模型。比如这里我们希望对文章title进行过滤,我们只需要修改ArticleView视图函数类即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ArticleView(viewsets.ModelViewSet):
"""文章列表视图类"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 只允许登录用户访问
# permission_classes = (IsAuthenticated, )
pagination_class = MyPageNumberPagination

def get_queryset(self):
"""筛选搜索结果"""
keyword = self.request.query_params.get('q')
# 没有搜索关键词则响应全部,否则响应过滤后的结果
if not keyword:
queryset = Article.objects.all()
else:
queryset = Article.objects.filter(title__icontains=keyword)
return queryset

def perform_create(self, serializer):
"""添加文章时绑定当前登录用户为文章的作者"""
# 将request.user与author绑定。调用create方法时执行如下函数。
serializer.save(author=self.request.user)

修改好视图类后,发送GET请求到/api/articles/?q=华为, 你将得到所有标题含有华为关键词的文章列表,这里显示一共有3条结果。

注意:DRF中你通过request.query_params获取GET请求发过来的参数,而不是request.GET。如果你希望获取从URL里传递的参数,你可以使用self.kwargs['param1']

假如你的URL配置如下所示:

1
re_path('^articles/(?P<username>.+)/$', AricleView.as_view()),

在视图中你可以通过self.kwargs['username']获取URL传递过来的用户名。

1
2
3
4
5
6
7
8
9
class ArticleView(generics.ListAPIView):
serializer_class = ArticleSerializer

def get_queryset(self):
"""
按用户名查询发表文章清单
"""
username = self.kwargs['username']
return Article.objects.filter(author__username=username)

当一个模型需要过滤的字段很多且不确定时(比如文章状态、正文等等), 重写get_queryset方法将变得非常麻烦,更好的方式是借助django-filter这个第三方库实现过滤。

使用django-filter

django-filter库包含一个DjangoFilterBackend类,该类支持REST框架的高度可定制的字段过滤。这也是小编推荐的过滤方法, 因为它自定义需要过滤的字段非常方便, 还可以对每个字段指定过滤方法(比如模糊查询和精确查询)。具体使用方式如下:

安装django-filter

1
pip install django-filter

django_filters添加到INSTALLED_APPS中去。

1
2
3
4
INSTALLED_APPS = [
...,
django_filters,
]

接下来你还需要把DjangoFilterBackend设为过滤后台。你可以在settings.py中进行全局配置。

1
2
3
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}

还可以在单个视图中使用它。

1
2
3
4
5
6
7
8
9
10
11
from django_filters.rest_framework import DjangoFilterBackend


class ArticleView(viewsets.ModelViewSet):
"""文章列表视图类"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 只允许登录用户访问
# permission_classes = (IsAuthenticated, )
pagination_class = MyPageNumberPagination
filter_backends = (DjangoFilterBackend,)

在类视图中使用django-filter时,你可以直接通过filter_fields设置希望过滤的字段,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
from django_filters.rest_framework import DjangoFilterBackend


class ArticleView(viewsets.ModelViewSet):
"""文章列表视图类"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 只允许登录用户访问
# permission_classes = (IsAuthenticated, )
pagination_class = MyPageNumberPagination
filter_backends = (DjangoFilterBackend,)
filter_fields = ['title', 'status']

如果你希望进行更多定制化的行为,你需要自定义FilterSet类,然后指定filter_class

自定义FilterSet类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import django_filters

from api.models import Article


class ArticleFilter(django_filters.FilterSet):
"""文章过滤类"""
# 搜索标题,不区分大小写包含
q = django_filters.CharFilter(field_name='title', lookup_expr='icontains')

class Meta:
model = Article
# 允许完全匹配标题和状态
fields = ('title', 'status')

接下来通过filter_class使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from api.filter.article import ArticleFilter


class ArticleView(viewsets.ModelViewSet):
"""文章列表视图类"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 只允许登录用户访问
# permission_classes = (IsAuthenticated, )
pagination_class = MyPageNumberPagination
# 过滤器
filter_backends = (DjangoFilterBackend,)
# filterset_fields = ['title', 'status']
# 自定义过滤器类
filter_class = ArticleFilter

你还可以看到REST框架提供了一个新的Filters下拉菜单按钮,包含我们自定义过滤器中设置的两个字段(标题和状态)的完全匹配和标题的包含匹配。可以帮助您对结果进行过滤(见上图标红部分)。

使用SearchFilter类

其实DRF自带了具有过滤功能的SearchFilter类,其使用场景与Django-filter的单字段过滤略有不同,更侧重于使用一个关键词对模型的某个字段或多个字段同时进行搜索。

SearchFilter类支持基于简单单查询参数的搜索,并且基于Django admin的搜索功能。

使用这个类,你还需要指定search_fields, 具体使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
class ArticleView(viewsets.ModelViewSet):
"""文章列表视图类"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 只允许登录用户访问
# permission_classes = (IsAuthenticated, )
pagination_class = MyPageNumberPagination
# 后端过滤器
filter_backends = (SearchFilter,)
# 允许搜索过滤的字段
search_fields = ('title',)

注意:这里进行搜索查询的默认参数名为?search=xxx。

SearchFilter类非常有用,因为它不仅支持对模型的多个字段进行查询,还支持ForeinKey和ManyToMany字段的关联查询。按如下修改search_fields, 就可以同时搜索标题或用户名含有某个关键词的文章资源列表。修改好后,作者用户名里如果有django,该篇文章也会包含在搜索结果了。

1
search_fields = ('title', 'author__username')

默认情况下,SearchFilter类搜索将使用不区分大小写的部分匹配(icontains)。你可以在search_fields中添加各种字符来指定匹配方法。

  • ’^’开始 - 搜索。
  • ’=’完全匹配。
  • ’@’全文搜索。
  • ’$’正则表达式搜索。

例如:search_fields = (‘=title’, )精确匹配title。

自定义SearchFilter类

默认SearchFilter类仅支持?search=xxx这个传递参数,你可以通过设置SEARCH_PARAM覆盖。另外你还可以重写get_search_fileds方法改变它的行为。下例中,当你按照?search=关键字&title_only=True提交请求时,它将只针对title进行查询。

1
2
3
4
5
6
7
8
9
10
11
from rest_framework.filters import SearchFilter


class MySearchFilter(SearchFilter):
"""自定义搜索过滤器"""

def get_search_fields(self, view, request):
# 如果url参数中包含title_only,将只针对title进行查询
if request.query_params.get('title_only'):
return ['title']
return super().get_search_fields(view, request)