Django + DRF 实现列表接口定制查询参数功能和多选查询

文章目录

  • 前言
  • 需求
  • 思路
    • 定制查询参数
    • 多选查询
      • 难点
      • 解决思路
  • 具体实现
    • 定制查询参数
    • 多选查询
  • 总结

前言

最近在使用 Django 配合 DRF (django-rest-framework) 开发 CD 系统调度器,遇到了这个需求。

之前做侧开,主要侧重于测试,也进行过这种列表接口的测试。也熟悉了标准的调用方法。现在主要侧重于开发,就轮到我开发这种接口了。

查了下 DRF 默认支持的查询机制和前端需要的有点不一样。所以在这里把具体实现记录下。

需求

手头上现有一个发布单的列表接口,可以返回目前已经创建的发布单信息,并结合 DRF 已经配置了序列化和分页。

现在需要接口可以支持使用请求参数来过滤返回的发布单列表信息,也就是添加查询参数。并且要支持某个参数的多选查询。

实际请求过来的 url 会在原先的 url 后添加上类似 ?name=first&status=0,1 这样的一串信息。它的意思是:使用 namestatus 来进行查询,返回 (namefirst) 且 (status01)

思路

定制查询参数

DRF 中提供了一个组件 SearchFilter (源码在此)来提供查询功能。该组件可以通过类属性 search_param 指定 url 中的查询关键字,通过类属性 search_fields 指定 查询关键字对应的数据表的字段。一个关键字可以对应多个字段。

SearchFilter 默认的匹配逻辑是:只要数据表中某条记录的某个指定字段满足匹配,就记为当前查询关键字的一条查询结果

举个例子,假设现在的情况是:

  1. 查询关键字是 name
  2. 查询的值是 first
  3. 数据表的匹配逻辑是 icontains
  4. 数据表中有三个字段 idpre_namepost_namesearch_fields 设置为关联 pre_namepost_name

则最终返回的查询结果是 pre_name post_name 字段的值包含 first 的记录的集合

SearchFilter 查询关键字对应的多个 数据表字段之间采用了 或逻辑DRF 中的一个 ListCreateAPIView 可以支持设定多个 SearchFilter,以列表的形式使用。每个 SearchFilter 可以定制对应的 search_paramsearch_fields ,多个 SearchFilter 的结果之间采用 与逻辑

多选查询

难点

SearchFilter 对于多选查询的默认逻辑是 与逻辑

举个例子,假设现在的情况是:

  1. 查询关键字是 status
  2. 查询的值是 0,1
  3. 数据表的匹配逻辑是 iexact
  4. 数据表中有三个字段 idnamestatussearch_fields 设置为关联 status

则最终返回的查询结果是 status 字段的值 等于 0 等于 1 的记录的集合。

这不是扯淡嘛。需求需要的是返回 status 字段的值 等于 0 等于 1 的记录的集合。

这里的难点就在于 SearchFilter 多选查询的逻辑被硬编码到代码中,预先并没有给出定制的地方。总不能直接覆盖对应的方法吧。虽然也不失为一种办法。

解决思路

解决思路的话,我认为是有三个的。分别是:

  1. 构造 SQL 自己定制查询。
    这里尽量使用 Django objects 提供的 raw 方法,官方教程点我,它内置了对 SQL 注入的防范。

    但我是 ORM 的仔,所以此方案 PASS

  2. 重写 filter_queryset 方法。
    filter_querysetSearchFilter 用来过滤查询集,返回最终查询结果。 重写 filter_queryset 其实就是继承 SearchFilter,把下图红框部分改成 operator.or_

    为了实现或逻辑,在一坨代码中修改这么个小地方。如果下个版本在这坨代码中出个 Bug,官方修正后,你是跟着改还是不改呢。为了避免这个选择,此方案依然 PASS

  3. 重写 get_search_terms 方法。
    get_search_terms 用于提取请求 url 中查询关键字对应的值。以逗号分隔,每个值是一个 term

    例如,url***?status=1,212 都是一个 termget_search_terms 返回 term 列表。从上图的 116-122 行可以看到每个 term 内部是采用 或逻辑term 之间是采用 与逻辑

    直接将 与逻辑 改成 或逻辑 就是刚刚说的第二个方法。

    而在 term 列表外再加个列表,并把数据表匹配逻辑改成 in,就是本方法。这样就不存在 term 之间的 与逻辑SearchFilter 默认没有给出 in 的匹配方式,这里就入乡随俗,用正则匹配 iregex。此方案 ACCEPTED

具体实现

定制查询参数

这个很简单,我直接贴代码了。实现的功能是:

  1. 查询关键字是 name
  2. 数据表的匹配逻辑是 icontains
  3. 查询数据表中 pre_namepost_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]

多选查询

多选查询实现的功能是:

  1. 查询关键字是 status
  2. 数据表的匹配逻辑是 iregex
  3. 查询数据表中 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

你可能感兴趣的:(Django,白菜系列--Python)