django-filter 库针对的场景是:根据用户的请求参数(HTTP query param)来筛选出返回的数据。它做的事情有:
- 校验请求参数(参数由 Filter 定义)
- 按用户设定的筛选规则构建 queryset
- 这一步是 核心,将过程式的筛选逻辑变成声明式
- 形成数据库结果集
django-filter 还可以用于 Django REST Framework。
使用示例
# 通过 FilterSet 定义用于筛选的请求参数。这个例子支持这些参数:
# price__lt, price__gt, release_date, release_date__year__gt
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = {
'price': ['lt', 'gt'],
'release_date': ['exact', 'year__gt'],
}
# View 函数实现
def product_list(request):
# 将用户请求参数(request.GET)及基础 queryset 提供给 ProductFilter
f = ProductFilter(request.GET, queryset=Product.objects.all())
return render(request, 'my_app/template.html', {'filter': f})
ProductFilter 会提供一个 Form(.form
),也可以通过其 .qs
访问到其根据请求参数生成的 queryset。模版的示例如下:
{% extends "base.html" %}
{% block content %}
<form method="get">
{{ filter.form.as_p }}
<input type="submit" />
</form>
{% for obj in filter.qs %}
{{ obj.name }} - ${{ obj.price }}<br />
{% endfor %}
{% endblock %}
核心概念
django-filter 的核心概念有:
- FilterSet:表示一组 filter
- Filter:
- 每个 filter 代表一个过滤条件
- Filter 有类型,如
BooleanFilter
,它们有对应的 Django form field,如django.forms.fields.NullBooleanField
- django-filter 使用 Django Form 的能力对请求参数做 validation
至于 Widget 和 Form 的能力,由于我不使用 Django 输出 HTML,没有去关注。
仅显示用户自己发表的内容
在 UGC 应用中,经常需要拉取用户自身的内容。这个可以通过 编写 一个自定义的 base queryset 来实现:
class ArticleFilter(django_filters.FilterSet):
class Meta:
model = Article
fields = [...]
@property
def qs(self):
parent = super().qs
author = getattr(self.request, 'user', None)
return parent.filter(author=author)
对单个 Filter 实现自定义的筛选逻辑
通过 Filter 类提供的 method 参数:
class F(FilterSet):
"""Filter for Books by if books are published or not"""
published = BooleanFilter(field_name='published_on', method='filter_published')
def filter_published(self, queryset, name, value):
# construct the full lookup expression.
lookup = '__'.join([name, 'isnull'])
return queryset.filter(**{lookup: False})
# alternatively, you could opt to hardcode the lookup. e.g.,
# return queryset.filter(published_on__isnull=False)
class Meta:
model = Book
fields = ['published']
通过多个 Filter 做组合筛选
django-filter 并没有给出解决方案。可能的方法有以下几种,但都不是很理想。Django Form 你可以实现一个 clean()
函数来做跨多个 field 的 validation,比如 field A 的值为 "email" 时 field B 必须是个合法的 Email 地址。django-filter 应该实现一个类似的机制。
实现自定义的 base queryset
为你的 FilterSet 实现 一个 base queryset。框架在调用 FilterSet.qs
时,已经对请求参数(Filter 来表达)做过校验了。缺点是 .qs
是用来表达 base queryset 的,而不是用来做筛选逻辑的。
通过 Filter 的 method 做 hack
django-filter 调用各 filter 定义的 method 参数来构建 queryset 时,各 filter(请求参数)已经通过 validation 了。可以用这样的方式做 hack:
class NoteFilterSet(filters.FilterSet):
topic = filters.ModelChoiceFilter(queryset=Topic.objects.all(), method='filter_topic', label='Topic')
recursive = filters.BooleanFilter(label='Recursive', method='filter_recursive')
def filter_topic(self, queryset, name, value):
"""
根据 topic 筛选 note。如果请求参数中 recursive 为 true,则筛选指定的 topic 及其子 topic。
django-filter 不支持多 filter 同时起作用,因此这里用了 self.form 来获取 recursive 的值。
"""
recursive = self.form.cleaned_data.get('recursive')
if recursive:
return queryset.filter(topics__in=list(value.descendants(include_self=True)))
else:
return queryset.filter(topics__in=[value])
def filter_recursive(self, queryset, name, value):
"""
这里的 recursive 参数不单独做 filter,它在 filter_topic 中被使用。
使用这个函数使得 django-filter 不尝试去为 recursive 寻找相对应的 field。
"""
return queryset
并不优雅,但是可以运行。
不在 django filter 上做此类逻辑
比如 DRF 搭配 django-filter 时,可以在 DRF 的 get_queryset() 做这种逻辑。好处是灵活;代价是可能需要自己实现 query param 的校验逻辑。