尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

django-filters 源码包实战:FilterSet 声明式过滤解决 Django 列表与 DRF 接口查询难题

django-filters 源码包实战:FilterSet 声明式过滤解决 Django 列表与 DRF 接口查询难题 简介django-filters是Django生态中用于构建复杂查询过滤器的库在RESTful API后端开发中十分常用。这份资源是django-filters的完整源码包面向有一定Django基础、希望深入理解过滤器机制或进行二次开发的开发者。资源共61个文件以Python源码为主包含filterset.py、filters.py、fields.py、widgets.py等核心模块同时提供Python 3.6编译产物pyc、多语言翻译文件po/mo以及少量HTML文档整体压缩包仅99KB。已有289人学习下载。通过研读源码可以透彻理解FilterSet的声明与过滤流程、各类内置过滤器的实现方式以及如何继承Filter类编写自定义过滤逻辑还能看到与Django REST Framework的集成层掌握DjangoFilterBackend的工作原理和lookup_expr、order_by、distinct等高级选项的用法。目录结构清晰便于按模块逐个对照学习是研究Django数据过滤机制的理想参考资料。1. 从最容易被低估的过滤器说起django-filters 源码包到底解决什么问题做 Django 列表页和 REST 接口的人大多被手写过滤逻辑折磨过请求带五六个参数视图里堆满if request.GET.get(...)判空再逐个往 queryset 上叠 filter加一个字段就重复一遍页面一多就失控。在 python django filter 这条技术线上django-filters 源码包就是终结这种手写姿势的答案把 queryset 过滤条件声明成 FilterSet同一套定义既能生成 HTML 表单也能直接接 DRF 后端还能处理跨表、范围、多选和排序。写列表页、管理后台和接口的从业者都值得拿它替代手写过滤。下面按我实际拆包使用的路径把原理、落地与踩坑一次讲透。2. FilterSet 的底牌声明式字段如何变成表单与 Q 查询django-filters 的源码结构其实很轻核心就三样东西FilterSet、Filter以及它内部基于 Django Form 生成的一套绑定表单。很多人第一次用的时候觉得它像魔法其实拆开看就是「先把过滤条件转成表单做校验再把合法的表单值拼成 queryset 的 filter 参数」。这一章不急着写页面先把这条链路看清楚后面配参数、排错才有依据。2.1 FilterSet 与 Filter 的分工先认识三个核心类FilterSet 是入口OrderFilter(request.GET, querysetOrder.objects.all())这一步同时干了三件事解析请求参数、生成表单实例、把过滤条件叠加到 queryset。而每一个过滤维度对应一个 Filter 实例。看一段最小声明import django_filters as filters class OrderFilter(filters.FilterSet): status filters.ChoiceFilter( choicesOrder.STATUS_CHOICES, ) min_amount filters.NumberFilter( field_nameamount, lookup_exprgte, ) class Meta: model Order fields []这里的ChoiceFilter和NumberFilter是最常用的两个子类。ChoiceFilter会生成带 choices 的下拉框NumberFilter通过field_name指向模型字段amountlookup_exprgte表示最终生成amount__gte查询。注意一个很容易看漏的点类属性min_amount是「请求参数名」而field_name是「模型字段名」两者可以不一致。默认情况下Filter 的请求参数名就是类属性名前端传?min_amount1000就能命中。Meta里的model和fields决定两件事模型哪些字段可以被过滤以及缺省时自动生成哪些 Filter。当fields写成列表时django-filters 按模型字段类型推断默认 FilterCharField 生成 CharFilter默认icontains模糊查IntegerField 生成 NumberFilter默认exact。这也是很多项目里fields [status, created_at]一行就够用的原因省去手动声明。当fields写成字典时控制粒度更细每种 lookup 自动展开成一个独立 Filterclass Meta: model Order fields { status: [exact], amount: [gte, lte], customer__name: [icontains], }这里字典的 value 是 lookup 列表请求参数名默认是「字段名__lookup」比如amount__gte1000、customer__name__icontains张。跨表字段名customer__name会生成 join 查询这是 Django ORM 的常规能力django-filters 只是把它搬到了声明式配置里。表单生成逻辑是 FilterSet 能直接渲染成 HTML 的根本原因每个 Filter 内部映射到一个 Django Form 字段CharFilter 对应 CharFieldNumberFilter 对应数字类字段ChoiceFilter 对应 ChoiceFieldDateFilter 对应 DateField。所以 FilterSet 本质上是一层「先转表单、再校验、最后拼 queryset」的包装不是独立于 Django Form 的另一套体系。2.2 一条过滤链路的完整时序request.GET 如何变成结果集把整条链路拉开执行顺序大概是五步实例化 FilterSet传入 request.GET 与初始 queryset内部先生成 form。访问.qs属性时先调用form.is_valid()。校验通过后遍历每个 Filter取form.cleaned_data里的值跳过空值。对每个非空值拼接field_name__lookup_exprvalue并 apply 到 queryset。按声明顺序依次叠加返回最终 qs。想观察各环节的中间产物可以用下面这段代码fs OrderFilter(request.GET, querysetOrder.objects.all()) fs.form.is_valid() print(fs.form.cleaned_data) # 已解析并完成类型转换的参数 print(fs.qs.query) # 拼好的 SQL 文本方便排查cleaned_data里的键是「类属性名」而不是模型字段名值已经过 Form 字段的类型转换比如字符串金额变成 Decimal日期字符串变成 date 对象。值如果是空字符串或 None对应的 Filter 会被跳过这是「过滤条件不生效」最常见的源头。源码里两个关键方法值得记住filter_queryset负责循环叠加每个 Filter 的filter方法负责把单条条件应用进去。叠加顺序就是你在 FilterSet 里声明属性的顺序Meta.fields 生成的 Filter 则按 Meta 里的顺序。如果两个 Filter 指向同一字段后声明的会作用在先声明的结果上比如先status再amount__gteSQL 是statusX AND amount1000逻辑没有歧义但如果你想做「同一字段的或关系」声明式写法做不到那就要走第 4 章的method自定义。2.3 惰性求值过滤链路里最容易忽略的一环Django QuerySet 是懒加载的django-filters 继承了这套语义。FilterSet 的qs属性每次访问都会在「当前已过滤的 queryset」基础上再执行一次过滤但有个隐藏前提如果某个 queryset 已经被求值过比如 list 过、遍历过后续再叠过滤条件Django 会直接返回缓存里的旧结果新条件全部白搭base_qs Order.objects.all() fs1 OrderFilter(request.GET, querysetbase_qs) result list(fs1.qs) # 这里触发求值base_qs 已缓存 fs2 OrderFilter(request.GET, querysetbase_qs) # 复用已求值的 querysetbase_qs一旦求值后面的过滤就不可靠。正确做法是每个 FilterSet 都从Model.objects.all()这类未求值的 queryset 出发不要在循环里反复复用同一个已求值对象。另外 FilterSet 不会改动你传入的原始 queryset它内部会做复制再过滤所以多个视图引用同一个 queryset 变量时过滤是隔离的但「隔离」不等于「防缓存」两者要分清。注意排查过滤不生效时先确认两件事——queryset 是否已被求值、参数名是不是类属性名。这两点占了七成以上的「过滤器静默失效」。3. 把 django-filters 用起来从安装到 ListView 与 DRF 的完整落地原理清楚了这一章直接落地。我会按「安装 → 列表页 → 参数选型 → 接 DRF」的顺序走一遍每步都给可抄的代码和参数说明。这套流程我在好几个内部管理后台项目里都用同一套路改改模型名和字段就能跑。3.1 最小可用配置三步把过滤接进列表页第一步安装pip install django-filter然后加进INSTALLED_APPS注意是带 s 的django_filters不是django_filter拼写错误是第一道高频坑INSTALLED_APPS [ django.contrib.admin, # ... django_filters, ]第二步定义 FilterSet第三步在视图里使用。列表页最省事的做法是用django_filters.views.FilterView它内部就是 ListView 套了一层 FilterSet取全部 queryset、套过滤、处理无结果都帮你包好了from django_filters.views import FilterView from .models import Order from .filters import OrderFilter class OrderListView(FilterView): model Order filterset_class OrderFilter template_name orders/order_list.html paginate_by 20 strict FalsestrictFalse要单独说默认strictTrue时表单校验失败比如金额框传了字母会直接抛 500strictFalse则返回空过滤结果而不是抛错。列表页面向真实用户时手改 URL 参数是常态我一般设成 False由模板兜底提示。模板里这样渲染过滤表单和结果form methodget action {{ filter.form.as_p }} button typesubmit筛选/button /form {% for order in filter.qs %} p{{ order.order_id }} - {{ order.amount }}/p {% empty %} p没有符合条件的数据/p {% endfor %}注意模板变量是filter不是filterset。渲染表单用filter.form遍历数据用filter.qsFilterView 默认已经把它们放进 context不要自己再查一次 queryset否则过滤条件全白搭。如果你习惯函数视图常见做法是把三件事压成一行def order_list(request): f OrderFilter(request.GET, querysetOrder.objects.all()) context {filter: f, object_list: f.qs} return render(request, orders/order_list.html, context)OrderFilter(request.GET, ...)内部自动完成了参数解析、表单校验和 queryset 过滤后面所有页面逻辑只跟f.qs打交道。order_id 这类编号字段如果要精确匹配记得在 FilterSet 里用CharFilter配合lookup_exprexact避免默认 icontains 带来的性能开销。3.2 参数选型lookup_expr、widget 与几个省事的内置 Filter声明 Filter 时最常改的三个参数是field_name、lookup_expr、widget。field_name决定作用到哪个模型字段支持跨表路径lookup_expr决定 SQL 操作符。常用的 lookup 我做了一个速查表lookup_exprSQL 含义搭配 Filter典型场景exactCharFilter / NumberFilter精确匹配iexactILIKE忽略大小写CharFilter编号、代码、状态icontainsLIKE %v%CharFilter名称模糊搜索gte / lte/NumberFilter / DateFilter数值、日期范围inIN (...)BaseInFilter 组合多选过滤isnullIS NULLBooleanFilter空值过滤下面一组配置覆盖了最常用的组合日期范围和数值范围可以直接用内置的类型省掉手写两个 Filterclass OrderFilter(filters.FilterSet): keyword filters.CharFilter( methodfilter_keyword, label关键词, ) amount_range filters.NumericRangeFilter( field_nameamount, lookup_exprrange, ) created_at filters.DateTimeFromToRangeFilter() class Meta: model Order fields [status, channel]DateTimeFromToRangeFilter是最省事的日期范围写法前端传created_at_after与created_at_before两个参数后端自动拼created_at__gte和created_at__lte。NumericRangeFilter同理生成amount_range_min与amount_range_max。这两个内置类型免去你手写两个 Filter 的重复劳动。widget参数控制的是表单控件。默认情况下 Filter 按字段类型自动映射控件但如果你想要 HTML 里出现 date 控件或自定义 class就显式指定created_at filters.DateFilter( field_namecreated_at, lookup_exprgte, widgetforms.DateInput(attrs{type: date}), )这里要特别提醒如果同一个字段既在 FilterSet 里显式声明、又出现在 Meta.fields 里两者会去重合并但同名时「显式声明的 Filter 优先于 Meta 自动生成的」。所以一旦你对某个字段做了自定义参数记得把它从 Meta.fields 里拿掉否则等于声明了两次行为会变得不可预测。如果你还需要前端按某个字段排序django-filters 准备了OrderingFilterclass OrderFilter(filters.FilterSet): ordering filters.OrderingFilter( fields( (created_at, created_at), (amount, amount), ), ) class Meta: model Order fields [status]OrderingFilter接受?ordering-amount这样的参数负号表示倒序值必须落在fields允许的范围内天然防注入。它会往 queryset 上加order_by注意它不返回独立结果集而是直接改排序。3.3 接入 DRF同一套 FilterSet 直接给接口复用django-filters 提供了官方 DRF 支持接入成本比列表页还低。先在 settings 里配置默认后端REST_FRAMEWORK { DEFAULT_FILTER_BACKENDS: [ django_filters.rest_framework.DjangoFilterBackend, ], }然后在视图集里挂filterset_classfrom django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets class OrderViewSet(viewsets.ModelViewSet): queryset Order.objects.select_related(customer).all() serializer_class OrderSerializer filter_backends [DjangoFilterBackend] filterset_class OrderFilter此时GET /api/orders/?statuspaidamount__gte1000会自动被 OrderFilter 解析并过滤前端拿到的就是过滤后的结果。需要注意filter_backends是显式列表如果外层 settings 配了DEFAULT_FILTER_BACKENDS而视图集也写了filter_backends以视图集的列表为准两者不会合并。另外 DRF 下strict参数不生效校验失败时由 DRF 返回 400 而不是 500比 FilterView 更友好。如果你想让接口文档自动带出过滤参数可以把 FilterSet 的声明当作团队沟通的接口契约每个字段的 label、lookup 一目了然省得单独维护一份过滤文档。4. 自定义过滤器的边界method、列表参数与跨表协同声明式 Filter 只解决「某个字段按某种 lookup 过滤」这一种情况。遇到或关系、跨字段搜索、空值特殊处理、列表型参数就得往自定义方向走。这一章讲清楚method的写法、列表参数的解析以及跨表过滤时的性能边界。4.1 method 过滤逻辑当声明式写法不够用的时候最典型的场景是「一个关键词框搜多个字段」比如订单号、客户姓名、客户手机号三个字段里任意匹配。声明式做不到「或」关系必须用methodclass OrderFilter(filters.FilterSet): keyword filters.CharFilter( methodfilter_keyword, label关键词, ) def filter_keyword(self, queryset, name, value): return queryset.filter( Q(order_id__icontainsvalue) | Q(customer__name__icontainsvalue) | Q(customer__phone__icontainsvalue) ) class Meta: model Order fields [status]methodfilter_keyword里的名字对应类方法名系统会调用filter_keyword(queryset, name, value)name是该 Filter 的类属性名这里是keywordvalue是 cleaned_data 里拆出来的过滤值。方法只负责返回 queryset不要在这里改 self 或写全局状态。这里有个容易踩的点方法名的前缀必须是filter_后半段必须与 Filter 的类属性名一致。如果你写了filter_keyword但 Filter 属性叫kw内部回调找不到方法直接报AttributeError。命名对齐是 method 写法的第一纪律。method 里返回的 queryset 会继续参与后面的叠加多个 method Filter 可以串联。但注意链式 filter 是「与」关系如果想让「关键词不匹配」时返回空集合直接queryset.none()更干净别用filter(pk__in[])这种绕法。想排除某些条件时用exclude但要小心exclude与「或」条件混合时的语义先写Q再取反最保险。4.2 列表型参数与去重BaseInFilter 与 distinct 的配合HTTP 参数支持重复值比如?id1id2但普通 Filter 只取最后一个值。要接收列表值django-filters 提供了BaseInFilter混合类它要跟具体类型 Filter 组合才能做校验from django_filters import BaseInFilter, NumberFilter class NumberInFilter(BaseInFilter, NumberFilter): pass class OrderFilter(filters.FilterSet): id_in NumberInFilter(field_nameid, lookup_exprin) status_in filters.BaseInFilter( field_namestatus, lookup_exprin, ) class Meta: model Order fields []BaseInFilter会把请求里的重复 key 或逗号分隔值拆成列表再交给底层 Filter 的 lookup 处理。?id_in1,2,3和?id_in1id_in2id_in3两种写法都能解析。组合NumberFilter是为了让每个元素都过类型校验否则in里混入非数字会在数据库层报错。与列表过滤强相关的另一个坑是重复记录。当你按一对多关系过滤比如订单按客户标签过滤SQL 会 join 关联表同一条订单可能出现在多行。FilterSet 不会帮你自动去重正确做法是重写qs属性class OrderFilter(filters.FilterSet): tag filters.CharFilter( field_namecustomer__tags__name, lookup_expriexact, ) property def qs(self): parent_qs super().qs return parent_qs.distinct()重写qs属性是给 FilterSet 统一补distinct的常用姿势。注意父类qs是property覆盖时也必须保持 property 语义不能改成普通方法否则外部f.qs取到的是方法对象而不是 queryset。4.3 跨表字段与性能边界过滤器别把查询拖垮field_namecustomer__name这类跨表路径虽然方便但会引入 join数据量大时要格外注意。第一select_related/prefetch_related要在视图层配好django-filters 不会帮你做关联查询优化。用 FilterView 时从get_queryset下手class OrderListView(FilterView): filterset_class OrderFilter def get_queryset(self): return Order.objects.select_related(customer).all()select_related处理 ForeignKey 单次 joinprefetch_related处理多对多和反向关联两者各管一摊别混用。第二避免在 FilterSet 声明里做需要立刻求值的表达式。FilterSet 的qs是惰性的但如果你在类属性阶段调用了数据库每次构造都会打库页面会明显变慢。第三优先用带索引的字段做前置过滤。status、customer_id这类有索引的字段先过滤后面的跨表条件再叠加SQL 执行计划会更合理。没有索引的icontains模糊搜索在百万级表上是灾难建议配上全文检索能力或限制可搜字段数量。第四不要在 method 里做聚合型过滤比如「按订单总金额过滤客户」。FilterSet 的定位是行级过滤聚合分析应该先annotate再走别的查询链路硬塞进来会让qs.query复杂到没法排查。5. 避坑排查五个翻车现场与解决思路滤波器这个东西报错并不可怕可怕的是「不报错但结果不对」。这一章总结我实际项目中遇到最多的五个翻车现场每条都按现象、原因、解决的顺序写基本都是血泪经验换来的。5.1 过滤器静默失效参数名与字段名对不上现象URL 传了statuspaid列表结果没任何过滤浏览器里表单也没渲染出对应输入框。原因最常见有两种。一是请求参数名用的是类属性名你在 Meta.fields 里用的是模型字段名请求却按status__exact传参数名错位二是field_name拼错如果恰好拼到另一个合法路径Django 不报错只是结果不符合预期。解决先打印fs.form.cleaned_data看参数有没有被解析再打印fs.qs.query看 WHERE 长什么样。大多数这类问题都不是玄学而是参数 key 与类属性名的错位。我一般在模板里先把 form 字段的 name 打印出来与前端实际传参对齐再核对field_name。5.2 空值过滤不生效IS NULL 不等于 None现象想过滤「没有填写备注的订单」传remark或remarkNone结果查出来的还是全表。原因空字符串在cleaned_data里被当成空值django-filters 的默认逻辑是「空值跳过该 Filter」所以传空串等于没过滤。而 SQL 里的空值判断要用IS NULL得走lookup_exprisnull。解决声明一个显式的布尔过滤器no_remark filters.BooleanFilter( field_nameremark, lookup_exprisnull, )请求?no_remarktrue会生成AND remark IS NULL。注意BooleanFilter接受的合法值是字符串true/false/1/0别传 Python 的True布尔对象除非你在 request.GET 阶段手工做了类型转换。5.3 DRF 里 filterset_class 配了但接口没过滤现象ViewSet 里写了filterset_classsettings 里也配了DEFAULT_FILTER_BACKENDS但请求带参数依旧返回全量数据。原因视图集一旦显式声明filter_backendssettings 里的默认后端不会合并进来反过来如果只在 settings 配了后端而 ViewSet 没写filterset_classDRF 会尝试从 model 推断推断失败就什么都不过滤。解决统一写法ViewSet 里显式写filter_backends [DjangoFilterBackend]和filterset_class OrderFilter不要依赖 settings 默认值。同时注意不要在一个视图集里同时挂 SearchFilter 又用带 method 的 FilterSet多个后端的执行顺序不好排查。5.4 多对多或跨表过滤后出现重复记录现象按客户标签过滤订单同一条订单出现在结果里多次分页总数也不对。原因join 到多对多中间表后一条父记录匹配多条子记录Django 默认不会去重。解决按第 4.2 节的姿势重写qs属性补distinct。注意distinct要在分页器取 count 之前生效只要从 FilterSet 返回的 queryset 已经是 distinct 的分页就不会有重复计数的坑。如果还要按某个字段去重Django 支持distinct(字段名)但 PostgreSQL 外的数据库对它的支持有限跨库项目先确认方言。5.5 FilterView 在表单校验失败时直接 500现象金额过滤框里填了abc页面抛 ValueError错误堆栈指向cleaned_data。原因strictTrue是 FilterView 的默认值校验失败时不吞异常直接把错误抛给 Django 的异常处理链路。设计如此是为了让开发者注意到参数不合法但对真实用户不友好。解决视图里加strictFalse让校验失败时返回空 queryset更稳妥的做法是重写get_filterset_kwargs或在校验失败时手动构造带错误提示的响应。内部管理端我一般保留strictTrue公开列表页必须strictFalse。6. 把 FilterSet 变成可复用的查询引擎动态生成与调试技巧最后讲两个让 FilterSet 从「一个页面的过滤器」升级成「整个项目的查询引擎」的技巧动态生成和统一调试。动态生成依赖 Python 的type()工厂能力。FilterSet 本质上就是 classMeta 也是 class所以可以用配置动态拼出来def build_filterset(model, field_config, nameDynamicFilterSet): meta type(Meta, (), {model: model, fields: field_config}) return type(name, (filters.FilterSet,), {Meta: meta})field_config既可以传字段名字符串列表也可以传上面说过的「字段名 → lookup 列表」字典。多租户后台很适合这套每个租户有一张过滤字段白名单配置视图层根据租户配置调用build_filterset同一个列表页模板就能服务不同权限的过滤需求。函数返回的是一个 FilterSet 类不是实例用法和手写类完全一致。调试技巧上有两个值钱的习惯。一是永远看qs.query而不是凭肉眼猜结果它会把最终 SQL 打出来join、where、distinct、order by 一目了然二是在 method 过滤逻辑里临时打印入参确认value到底经过了什么转换def filter_keyword(self, queryset, name, value): print(name, value) # 定位到这里时先确认入参 return queryset.filter(...)我自己现在每个 FilterSet 配好后都会跑一段冒烟脚本遍历所有声明的 Filter分别传合法值、空值、错误类型值逐个打印qs.query核对 WHERE 是否符合预期。这套脚本不花多少时间但能在一开始就拦住 5.1 和 5.2 那类「不报错但结果不对」的坑。从那以后凡是接 FilterSet 的列表页和接口我都会强制走一遍「先打印 cleaned_data、再核对 qs.query、最后跑冒烟脚本」的流程。这套源码包你也可以按同样的方式拆一遍先看 filters 模块里声明的每个 Filter 是否都落到了 SQL 上再决定要不要改参数。希望帮到你。本文还有配套的精品资源点击获取
返回列表