简介:django-filters 源码包包含库的完整 Python 实现,适合已有 Django 基础、希望深入理解查询过滤机制的后端开发者。django-filters 常被用于 RESTful API 的动态筛选场景,通过 FilterSet 类声明过滤规则,支持多种过滤器类型、自定义 Filter,并能与 Django REST Framework 无缝集成,适用于需要灵活筛选数据的接口开发场景。压缩包格式为 RAR,共 61 个文件,大小约 99KB,主要包含 16 个 Python 源文件、15 个 pyc 编译文件、14 个 po 与 13 个 mo 多语言翻译文件,以及 3 个 HTML 模板。源码覆盖 filterset、fields、widgets、rest_framework/backends 等核心模块,同时包含 constants、conf、utils 等基础支撑文件;po/mo 文件提供国际化翻译,HTML 模板用于渲染过滤表单,目录结构清晰。已有 289 人学习下载。研读这份源码,可以掌握 FilterSet 的声明方式、字段与查找表达式映射、过滤结果集的组装逻辑,从 FilterSet 元类收集字段到 DjangoFilterBackend 将查询参数作用于 queryset 的完整链路,为定制过滤行为或排查复杂筛选问题提供直接参考。
1. django-filter 源码包:正在发生的查询参数到 ORM 条件的映射
做 Web 开发的人大概都有过这种经历:列表页要加筛选,前端传过来一堆参数,后端要按这些参数过滤数据库。最朴素的做法是拿if判断每个字段,写上十几个分支,页面一多,每个视图里都是重复的过滤代码。django-filter 这个包解决的就是这档子事——它把筛选逻辑声明成一个 FilterSet 类,通过内部的 Filter 对象和字段类型推断,把 HTTP 查询参数转换成语义化的 ORM 过滤条件,无论配 Django 视图还是配合 DRF 的 filter_backends,都能少写大半截样板代码。这份源码包适合正在做管理后台、报表查询、API 列表接口的 Python 开发,它能让你看明白过滤条件到底是在哪一层被构造出来的,也适合想把现有查询接口里那些手写 if 链替换成可配置方案的人。本文基于源码包的文件结构,讲清楚它的工作原理、通用配置方式,以及几个容易让人卡半天的注意点。
2. 核心抽象与选型理由:FilterSet、Filter 与字段自动映射的关系
2.1 FilterSet 类声明的过滤字段
django-filter 的入口是FilterSet,它不是简单地接受一个 queryset,而是把过滤逻辑组织成声明式结构。常见的写法是:
from django_filters import FilterSet, CharFilter, NumberFilter from myapp.models import Order class OrderFilter(FilterSet): customer_name = CharFilter(field_name='customer__name', lookup_expr='icontains') min_amount = NumberFilter(field_name='total_amount', lookup_expr='gte') max_amount = NumberFilter(field_name='total_amount', lookup_expr='lte') class Meta: model = Order fields = ['status', 'created_at']类里声明的CharFilter、NumberFilter是具体的过滤器件,field_name指向模型字段或关系字段的路径,lookup_expr决定用哪一种查询表达式。Meta 里定义model和fields,前者指定可调用的模型,后者列出需要开放过滤的字段列表,声明在类里的 Filter 会与 Meta 中的字段合并生效。当请求进来带?customer_name=张三时,FilterSet 会拿到这个参数值,用icontains生成类似customer__name LIKE '%张三%'的条件。
这套设计的价值在于把数据校验、查询表达式、字段绑定放到了同一个类中,相比在视图里写if request.GET.get('customer_name')再拼接条件,语义更集中,换模型换查询只改声明即可。如果你接手过一个列表页存在十几种筛选条件的项目,把它改写成 FilterSet 之后,视图代码会整洁很多。
2.2 Filter 类与 Django 查询表达式的对应关系
Filter类是整个包的地基,它内部依赖lookup_expr把过滤值翻译成 queryset 的过滤条件。常见的 lookup_expr 包括exact、iexact、contains、icontains、gt、gte、lt、lte、in、isnull等。源码包里值得关注的是这些表达式与查询参数的处理逻辑:当请求传入?status=paid时,status字段的默认exact表达式会执行queryset.filter(status='paid');当请求传入?amount__gte=100这种带双下划线的形式时,django-filter 也能解析出lookup_expr并套用到对应字段上。
class OrderFilter(FilterSet): status = CharFilter(field_name='status') amount_range = CharFilter(field_name='total_amount', method='filter_amount_range') def filter_amount_range(self, queryset, name, value): parts = value.split(',') if len(parts) == 2: return queryset.filter(total_amount__gte=parts[0], total_amount__lte=parts[1]) return queryset这里展示了method参数的作用——它允许你接管过滤逻辑,不再依赖默认的 lookup_expr。name参数接收当前 filter 的名称,value 是请求传入的值。你可以在 method 里基于原有 queryset 做任意二次过滤,比如把金额区间从前端传的min,max字符串里拆出来,再映射到两个 ORM 条件。参数说明:field_name缺省时用声明名称作为模型字段名;lookup_expr缺省为exact;method一旦指定,字段默认的过滤行为就会失效,完全走自定义逻辑。
实际项目里,金额区间、日期区间这类需求并不少见,直接使用NumberFilter配合gte/lte就能满足,但如果前端把区间封装为一个字段,那method就是标准的处理手段。
2.3 Filter 字段类型与表单校验的关系
django-filter 与 Django Form 关系密切,每个 Filter 内部都嵌套一个field_class,默认根据字段类型自动指定。比如CharFilter使用CharField做校验,NumberFilter使用IntegerField或FloatField,BooleanFilter使用BooleanField。它的过滤器基类在校验失败时会忽略该参数,而不是返回 500 错误,这是很多开发者容易忽略的行为。
from django_filters import NumberFilter class OrderFilter(FilterSet): min_amount = NumberFilter(field_name='total_amount', lookup_expr='gte')当请求传入?min_amount=abc时,内部表单字段校验不通过,该条件会被静默丢弃,queryset 不受影响。如果希望参数非法时返回报错,需要重写 Filter 类或在视图层单独处理。一个值得注意的点是 BooleanFilter 只接收true、false、1、0,若前端传的是True(首字母大写)同样会被当作无效值丢弃。这属于源码包里隐藏较深的一类细节,排查问题时不能只盯后端视图逻辑。
3. 与 Django/DRF 视图整合:过滤、分页、排序一次到位
3.1 在 DRF 环境中接入 django-filter
DRF 与 django-filter 是常见的组合。源码包里自带DjangoFilterBackend,在 DRF 视图集中声明后,?field=value的参数就会自动应用 FilterSet。一个最小可用的配置片段如下:
from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets from myapp.models import Order from myapp.serializers import OrderSerializer from myapp.filters import OrderFilter class OrderViewSet(viewsets.ModelViewSet): queryset = Order.objects.all() serializer_class = OrderSerializer filter_backends = [DjangoFilterBackend] filterset_class = OrderFilter这一步做了几件事:filter_backends负责把请求参数转交给过滤器处理,filterset_class指定过滤器的配置来源。DRF 启动时,会在get_queryset()阶段先取全部 queryset,再调用 FilterSet 执行过滤,随后进入分页与序列化环节。参数方面,注意filterset_class与filterset_fields这两个属性,前者提供完整 FilterSet,后者只是简单指定可过滤字段的列表。若同时存在,源码优先使用filterset_class。如果项目中很多视图只用最简单的几个字段筛选,filterset_fields = {'status': ['exact'], 'created_at': ['gte', 'lte']}是更轻量的方案,不必每个视图都新建一个 FilterSet 类。
3.2 普通 Django ListView 中使用 FilterSet
并非所有项目都使用了 DRF,django-filter 同样支持传统的 function view。核心处理方法是把request.GET作为初始数据传给 FilterSet,再把返回的qs交给分页器渲染。
from django.shortcuts import render from django.core.paginator import Paginator from myapp.filters import OrderFilter def order_list(request): f = OrderFilter(request.GET, queryset=Order.objects.prefetch_related('items')) page_obj = Paginator(f.qs, 20).get_page(request.GET.get('page')) context = {'filter': f, 'page_obj': page_obj} return render(request, 'orders/list.html', context)模板中可以动态渲染过滤表单:
<form method="get"> {{ filter.form.as_p }} <button type="submit">筛选</button> </form>这里的filter.form是从 FilterSet 自动生成的表单对象,字段顺序与类中声明顺序一致,可选值也继承了模型字段的限制。相比手写 HTML 表单,这种方式能保持参数名与 FilterSet 声明的一致,避免出现前端传min_amount后端读minAmount这种低级错误。
如果要在业务代码里手动获取过滤后的数值,用f.qs.count()即可。注意f.qs每次读取都会重新执行过滤逻辑,性能敏感型场景下应赋值给中间变量再复用。
3.3 分页与排序同时存在时的工作顺序
分页与 FilterSet 的协作逻辑经常被误解。DRF 的分页器和排序器位于过滤之后执行,三者嵌套顺序通常是过滤 → 排序 → 分页。以下配置展示了同时使用 django-filter 与 DRF 排序、分页的完整方式:
from rest_framework import viewsets from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter from myapp.models import Order from myapp.serializers import OrderSerializer from myapp.filters import OrderFilter class OrderViewSet(viewsets.ModelViewSet): queryset = Order.objects.all() serializer_class = OrderSerializer filter_backends = [DjangoFilterBackend, OrderingFilter] filterset_class = OrderFilter ordering_fields = ['created_at', 'total_amount'] ordering = ['-created_at']源码中的执行顺序是filter_queryset内部先跑DjangoFilterBackend,再跑后续的OrderingFilter和分页器。如果在 FilterSet 的 method 中写了基于排序字段的强大过滤逻辑,要先确认排序是在过滤完成后才介入的。有个常见的误区是在 FilterSet 中尝试对ordering参数做筛选,这并不会生效,因为OrderingFilter是独立的组件。
对于普通分页场景,DRF 默认使用PageNumberPagination,它依赖请求中的page参数,如果项目要求page_size参数,需要重写分页类。django-filter 给出的qs是一个惰性 queryset,在执行分页之前不会触碰数据库,因此性能损耗可以接受。
4. 动态构建过滤条件:从请求参数生成 FilterSet
4.1 基于用户输入动态构建关键字过滤器
在实际项目中,可能面临搜索框需要同时匹配多个字段的场景,比如搜索订单号、客户名、手机号。这时使用CharFilter加method既可以直接写多个字段的 OR 条件,也可以通过Q对象组合。常规实现如下:
from django_filters import FilterSet, CharFilter from django.db.models import Q from myapp.models import Order class OrderSearchFilter(FilterSet): keyword = CharFilter(method='filter_keyword') class Meta: model = Order fields = [] def filter_keyword(self, queryset, name, value): return queryset.filter( Q(order_no__icontains=value) | Q(customer__name__icontains=value) | Q(customer__phone__icontains=value) )method 里的参数name固定为过滤器字段名,value为当前请求参数的值。返回的是一个新的 queryset,原 queryset 不变。这种写法把跨表、多字段的搜索逻辑隔离到一个方法中,视图层不需要关心搜索字段的组成。Meta.fields声明为空是因为我们不希望其他字段被自动纳入过滤。
需要注意 method 的触发时机:django-filter 每次调用 FilterSet 的filter_queryset时会遍历所有 Filter,不论值是否为空,都会执行 method。这意味着如果你在 method 里对空值做了特殊处理,要显式判断并返回原 queryset,避免条件意外过滤掉全部数据。
4.2 使用 request 数据动态生成筛选表单
有业务场景是将用户选择的列作为过滤依据。FilterSet 也支持在实例化时从 request 中读取额外参数来决定是否启用某些过滤字段。例如不同的用户角色能看到的订单范围不同。
from django_filters import FilterSet, NumberFilter class OrderFilter(FilterSet): min_amount = NumberFilter(field_name='total_amount', lookup_expr='gte') class Meta: model = Order fields = ['status'] def __init__(self, data, *args, **kwargs): self.user_role = kwargs.pop('user_role', None) super().__init__(data, *args, **kwargs) if self.user_role == 'manager': self.filters['min_amount'].field.widget.attrs['readonly'] = True这里在__init__阶段根据传入的角色修改了filters字典中某个字段的表单属性。需要注意,FilterSet 的全部可用过滤器存放在self.filters字典中,修改它的时机一定要在调用super().__init__之后,否则字典里还没有对应的 Filter 对象。如果你只是想在某种条件下禁用某个过滤器,可以使用del self.filters['field_name'],这样请求中含有该参数时也不会参与过滤。
4.3 全局配置与多项目复用
django-filter 支持通过DJANGO_FILTERS_URL_QUERY参数控制过滤器的传参风格。全局配置可写在 Django 的 settings.py 中:
# settings.py DJANGO_FILTERS_URL_QUERY = True当DJANGO_FILTERS_URL_QUERY为 True 时,FilterSet 会按 URL 查询参数解析lookup_expr,允许?status__in=pending,paid这类带双下划线的写法。为 False 时,默认只允许显式声明的 Filter 字段参与过滤。多数项目保持默认即可,但如果你恰好需要批量状态筛选,可以统一改为 True,同时在每个需要解析的字段上加一个Filter类的lookup_expr声明。
源码包里另一个常用的可配置项是FILTERS_EMPTY_CHOICE_LABEL,用于控制空值选项的显示文案。在处理多选与空选混合的逻辑时,这个配置能减少不少表单层面的困惑。调整后不用迁移数据库,重启服务即可生效。
5. 避坑指南:django-filter 使用中的常见问题与排查思路
5.1 空参数返回全部数据的问题
现象:请求?status=时,本意是不传条件,但接口返回了空列表,或者反过来返回了全部数据。
原因:django-filter 对空字符串的处理取决于字段类型。CharFilter在遇到空字符串时默认认为该参数无值,会跳过过滤;但NumberFilter遇到空字符串会转换为 None,同样跳过。不过当你使用method自定义过滤时,空值也会进入 method 中,如果方法内部未判断空值就执行.filter(),可能会导致条件匹配不到数据或匹配全部数据。
解决:在自定义 method 开头显式判断请求值:
def filter_keyword(self, queryset, name, value): if not value: return queryset return queryset.filter(customer__name__icontains=value)这能保证参数没传或传空时,查询逻辑跳过该方法。如果使用了多个 Filter 做组合条件,建议在视图层先清理参数字典,把空字符串删除后再实例化 FilterSet,可以大幅减少无效分支。
5.2 related 字段过滤不生效
现象:FilterSet 声明customer__name = CharFilter(...),前端传来?customer__name=张三,过滤结果没有变化。
原因:Meta 中fields只列出了模型本身字段,customer__name这种跨关系字段需要在类中独立声明,且不能使用 Meta 的fields自动生成机制。部分用户误解为fields声明了就能自动支持所有关系路径透传,导致外键筛选失效。
解决:在类中显式声明 Filter 对象,指定field_name为跨表路径:
class OrderFilter(FilterSet): customer_name = CharFilter(field_name='customer__name', lookup_expr='icontains') class Meta: model = Order fields = ['status']注意前端传的参数名应是customer_name而不是customer__name。
5.3 多对多字段过滤出现重复数据
现象:对标签、分类等多对多字段做筛选时,结果中出现重复记录。
原因:django-filter 对多对多字段使用 JOIN 查询,当过滤条件匹配同一实体多条关联记录时,ORM 返回的 queryset 会出现重复行。
解决:在方法里对查询结果调用.distinct():
def filter_tags(self, queryset, name, value): return queryset.filter(tags__name__icontains=value).distinct()进一步的优化是在 FilterSet 的__init__中预先prefetch_related或select_related关联字段,减少额外查询损耗。
5.4 BooleanFilter 不支持显式 False 过滤
现象:声明is_active = BooleanFilter(field_name='is_active'),前端传?is_active=false时结果没有正确过滤。
原因:源码中BooleanFilter依赖表单的BooleanField,它只承认true、false、1、0,且要求值必须是小写。若前端传了False,校验不通过,该参数会被静默忽略。
解决:确认前端传参格式是小写布尔值;如果前端不可控,可以改用ChoiceFilter加选项:
from django_filters import ChoiceFilter class OrderFilter(FilterSet): is_active = ChoiceFilter(choices=[('true', '是'), ('false', '否')])这种方法能更宽容地处理不同大小写的布尔值。
5.5 自定义 Filter 顺序影响过滤结果
现象:FilterSet 类中声明多个 Filter 时,过滤顺序与声明顺序不一致,导致某些条件下查询结果错误。
原因:FilterSet 内部按字典序遍历self.filters,字典顺序在 Python 3.7+ 与插入顺序一致,但在 FilterSet 子类中,Meta 字段生成的 Filter 排在了类属性 Filter 之后,导致声明顺序被覆盖。
解决:如果对顺序敏感,可在__init__中手动重新排列self.filters,或统一将过滤器集中放 Meta 的fields中声明:
class Meta: model = Order fields = { 'status': ['exact'], 'created_at': ['gt', 'lt'], }这种声明方式会按字典中的字段顺序生成过滤器,便于控制执行顺序。
6. 进阶技巧:用 Q 对象组合 OR 条件,替代复杂 method
遇到多条件 OR 查询,最直接的解决方案是重写 Filter 的filter方法,在保持 FilterSet 声明式风格的同时,获得更灵活的查询控制能力。相比 method 在 FilterSet 层操作整个 queryset,自定义 Filter 可以在单个字段维度上做逻辑控制,复用性更高。
from django_filters import Filter from django.db.models import Q class MultiFieldSearchFilter(Filter): def filter(self, qs, value): if not value: return qs search_terms = value.split() q_objects = Q() for term in search_terms: q_objects &= ( Q(order_no__icontains=term) | Q(customer__name__icontains=term) | Q(customer__phone__icontains=term) ) return qs.filter(q_objects)from django_filters import FilterSet from myapp.models import Order class OrderFilter(FilterSet): keyword = MultiFieldSearchFilter(field_name='keyword') class Meta: model = Order fields = ['status']上面的逻辑把keyword按空格拆成多个词,词与词之间使用与关系,每个词内部使用或关系,这就实现了类似搜索引擎的“包含全部关键词”查询效果。field_name在这里不指向真实字段,仅作为参数名存在,实际查询条件完全由自定义filter方法决定。
这类 Filter 的好处是能够独立测试,不依赖 FilterSet 的实例状态。我可以直接构造一个Order.objects.all()的 queryset,再调用filter方法验证每一项查询条件是否正确拼接。相比于在 FilterSet 方法中逐步调试,这种模式隔离性更好。
使用这个方法的时候要留意distinct()的位置。如果查询涉及关联表,同样需要在返回前调用.distinct()避免重复数据。具体在 Filter 方法中实现为:
def filter(self, qs, value): if not value: return qs q_objects = Q() ... return qs.filter(q_objects).distinct()从那以后我每次在 FilterSet 中使用关联字段查询时,都会提前把是否存在多对多关系、是否需要去重视察一遍,养成先写查询在 Control 台观察生成的原生 SQL 的习惯。多次翻车之后,验证这一步已经成了固定流程,希望帮到你。
本文还有配套的精品资源,点击获取