相信你已经搭建好了django+rest_framework,并且成功启动了你的项目。接下来如果想要使用django_filters或者django_rest_framework_filters过滤器,那么你还需如下配置:
# settings.py INSTALLED_APPS = [ ... 'rest_framework', 'django_filters', 'rest_framework_filters' ] REST_FRAMEWORK = { 'DEFAULT_FILTER_BACKENDS': ( 'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework_filters.backends.RestFrameworkFilterBackend', 'rest_framework.filters.SearchFilter', 'rest_framework.filters.OrderingFilter', ... ), } rest_framework_filters是django_filters的高级扩展,使用了rest_framework_filters就无需在引入django_filters。
以上是全局配置方法, 当然也可以在每个viewset中配置独有的过滤器后端。
以上配置完成,就可以愉快的和前端小伙伴联调测试了。
如果你还不止步于此,想一探究竟django filter是怎么做到如此简单方便又好用的设计呢?或者你遇到了更为复杂需求,比如正则过滤、多值查询、复杂跨表查询等,这就需要对源码有一定了解了。下面进入正题:
以下是drf部分源代码:
class GenericAPIView(views.APIView):
queryset = None
serializer_class = None
...
lookup_field = 'pk'
lookup_url_kwarg = None
# The filter backend classes to use for queryset filtering
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
def filter_queryset(self, queryset):
"""
Given a queryset, filter it with whichever filter backend is in use.
You are unlikely to want to override this method, although you may need
to call it either from a list view, or from a custom `get_object`
method if you want to apply the configured filtering backend to the
default queryset.
"""
for backend in list(self.filter_backends):
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
...
class ListModelMixin:
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
...
当列表页请求过来时,ListModelMixin.list函数调用GenericViewSet.filter_queryset对查询集过滤,filter_queryset这个函数的作用是循环遍历过滤器后端并初始化,并且链式调用每个过滤器的filter_queryset方法,对查询集进行过滤。由此也可以看出django配置文件中配置的DEFAULT_FILTER_BACKENDS参数取的是各个过滤器的交集结果,且的关系。
下面继续看DjangoFilterBackend.filter_queryset方法:
class DjangoFilterBackend(metaclass=RenameAttributes):
filterset_base = filterset.FilterSet
raise_exception = True
...
def filter_queryset(self, request, queryset, view):
filterset = self.get_filterset(request, queryset, view)
if filterset is None:
return queryset
if not filterset.is_valid() and self.raise_exception:
raise utils.translate_validation(filterset.errors)
return filterset.qs
...
def get_filterset_kwargs(self, request, queryset, view):
return {
'data': request.query_params,
'queryset': queryset,
'request': request,
}
def get_filterset(self, request, queryset, view):
filterset_class = self.get_filterset_class(view, queryset)
if filterset_class is None:
return None
kwargs = self.get_filterset_kwargs(request, queryset, view)
return filterset_class(**kwargs)
...
def get_filterset_class(self, view, queryset=None):
"""
Return the `FilterSet` class used to filter the queryset.
"""
filterset_class = getattr(view, 'filterset_class', None)
filterset_fields = getattr(view, 'filterset_fields', None)
...
DjangoFilterBackend.filter_queryset 作用是实例化filterset,并且返回filterset的qs属性。到此也就完成了过滤任务。
filterset是什么呢?其实就是你在viewset中配置的filterset_class自定义过滤器属性。
filterset.qs是什么呢?往下看。
继续看这个filterset_class:
你在viewset中配置的filterset_class应该是这个样子的:
import django_filters class ProductFilter(django_filters.FilterSet): name = django_filters.CharFilter(lookup_expr='icontains') class Meta: model = Product fields = ['price', 'release_date']
也就是说上面讲到的DjangoFilterBackend.filter_queryset是实例化了你这个自定义的过滤器,最终返回了你这个过滤器的qs属性。
再看django_filters.FilterSet这个父类:
class FilterSet(BaseFilterSet, metaclass=FilterSetMetaclass):
pass
class BaseFilterSet:
def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
if queryset is None:
queryset = self._meta.model._default_manager.all()
model = queryset.model
self.is_bound = data is not None
self.data = data or {}
self.queryset = queryset
self.request = request
self.form_prefix = prefix
self.filters = copy.deepcopy(self.base_filters)
...
@property
def form(self):
if not hasattr(self, '_form'):
Form = self.get_form_class()
if self.is_bound:
self._form = Form(self.data, prefix=self.form_prefix)
else:
self._form = Form(prefix=self.form_prefix)
return self._form
def filter_queryset(self, queryset):
for name, value in self.form.cleaned_data.items():
queryset = self.filters[name].filter(queryset, value)
assert isinstance(queryset, models.QuerySet), \
"Expected '%s.%s' to return a QuerySet, but got a %s instead." \
% (type(self).__name__, name, type(queryset).__name__)
return queryset
@property
def qs(self):
if not hasattr(self, '_qs'):
qs = self.queryset.all()
if self.is_bound:
# ensure form validation before filtering
self.errors
qs = self.filter_queryset(qs)
self._qs = qs
return self._qs
...
终于找到qs属性了,这也就是真正对字段进行过滤的地方了。qs通过调用BaseFilterSet.filter_queryset方法对字段进行过滤。
对哪些字段过滤则是在BaseFilterSet.form做了处理。form中关注一个self.data属性,这个属性是在FilterSet实例化时被赋值,也就是在上面提到的DjangoFilterBackend.filter_queryset 中实例化FilterSet时被赋值,即self.data=request.query_params。
BaseFilterSet.filter_queryset中还有一个关键的属性self.filters。这个属性定义了如何对request.query_params参数过滤处理。从上面代码可以看出该参数拷贝自copy.deepcopy(self.base_filters),而base_filters则由BaseFilterSet.get_filters()调用得到:
class FilterSetMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['declared_filters'] = cls.get_declared_filters(bases, attrs)
new_class = super().__new__(cls, name, bases, attrs)
new_class._meta = FilterSetOptions(getattr(new_class, 'Meta', None))
new_class.base_filters = new_class.get_filters()
class BaseFilterSet:
@classmethod
def get_filters(cls):
"""
Get all filters for the filterset. This is the combination of declared and
generated filters.
"""
# No model specified - skip filter generation
if not cls._meta.model:
return cls.declared_filters.copy()
# Determine the filters that should be included on the filterset.
filters = OrderedDict()
fields = cls.get_fields()
undefined = []
for field_name, lookups in fields.items():
field = get_model_field(cls._meta.model, field_name)
# warn if the field doesn't exist.
if field is None:
undefined.append(field_name)
for lookup_expr in lookups:
filter_name = cls.get_filter_name(field_name, lookup_expr)
# If the filter is explicitly declared on the class, skip generation
if filter_name in cls.declared_filters:
filters[filter_name] = cls.declared_filters[filter_name]
continue
if field is not None:
filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr)
# Allow Meta.fields to contain declared filters *only* when a list/tuple
if isinstance(cls._meta.fields, (list, tuple)):
undefined = [f for f in undefined if f not in cls.declared_filters]
if undefined:
raise TypeError(
"'Meta.fields' must not contain non-model field names: %s"
% ', '.join(undefined)
)
# Add in declared filters. This is necessary since we don't enforce adding
# declared filters to the 'Meta.fields' option
filters.update(cls.declared_filters)
return filters
get_filters中关注一下三行:
...
filters = OrderedDict()
# get_fields获取你自定的过滤器中Meta.fields属性,上面举例中的['price', 'release_date']
fields = cls.get_fields()
...
# 经过一系列处理,会根据字段类型自动将['price', 'release_date']生成对应的filter,
# 形如:
#filters = {'price': django_filters.filters.NumberFilter}
# declared_filters 则是自定义过滤器中声明的filter,
# 形如 declared_filters = {'name':django_filters.CharFilter(lookup_expr='icontains')
}
filters.update(cls.declared_filters)
分析到此,要查询的字段self.form有了,使用的过滤器self.filter也有了,那么queryset = self.filters[name].filter(queryset, value),这句代码也就不难理解了。
但还有一点就是对filter这个函数的理解,看下源码就不难理解了:
class Filter:
...
def filter(self, qs, value):
if value in EMPTY_VALUES:
return qs
if self.distinct:
qs = qs.distinct()
lookup = '%s__%s' % (self.field_name, self.lookup_expr)
qs = self.get_method(qs)(**{lookup: value})
return qs
class CharFilter(Filter):
field_class = forms.CharField
相信你看到这里,也就了解了django filter的整个来龙去脉。相信你也能自定义如下一个支持正则表达式的过滤器:
import django_filters
class RegexFilter(filters.CharFilter):
@classmethod
def valid(cls, value):
try:
re.compile(value)
return True
except re.error as e:
raise Error('正则表达式语法错误')
def filter(self, qs, value):
self.valid(value)
if value in EMPTY_VALUES:
return qs
if self.distinct:
qs = qs.distinct()
lookup = '%s__%s' % (self.field_name, self.lookup_expr)
qs = self.get_method(qs)(**{lookup: value})
return qs
class ProductFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='icontains')
name__regex = RegexFilter(field_name='name', lookup_expr='regex', label='名称')
class Meta:
model = Product
fields = ['price', 'release_date']
# /product/products/?name=123 查询包含123的产品
# /product/products/?name__regex=^123.* 查询以123开头的产品
django_rest_framework_filters源码大同小异,就不详细探讨了。
完活,收工。