> 文章列表 > django_filters、django_rest_framework_filters源码解析

django_filters、django_rest_framework_filters源码解析

django_filters、django_rest_framework_filters源码解析

相信你已经搭建好了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是怎么做到如此简单方便又好用的设计呢?或者你遇到了更为复杂需求,比如正则过滤、多值查询、复杂跨表查询等,这就需要对源码有一定了解了。下面进入正题:

一、filter backend是何时、何地、如何被DRF调用的

以下是drf部分源代码:

class GenericAPIView(views.APIView):queryset = Noneserializer_class = None...lookup_field = 'pk'lookup_url_kwarg = None# The filter backend classes to use for queryset filteringfilter_backends = api_settings.DEFAULT_FILTER_BACKENDSdef 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 needto call it either from a list view, or from a custom `get_object`method if you want to apply the configured filtering backend to thedefault 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.FilterSetraise_exception = True    ...def filter_queryset(self, request, queryset, view):filterset = self.get_filterset(request, queryset, view)if filterset is None:return querysetif 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 Nonekwargs = 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 = Productfields = ['price', 'release_date']

也就是说上面讲到的DjangoFilterBackend.filter_queryset是实例化了你这个自定义的过滤器,最终返回了你这个过滤器的qs属性。

再看django_filters.FilterSet这个父类:

class FilterSet(BaseFilterSet, metaclass=FilterSetMetaclass):passclass 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.modelself.is_bound = data is not Noneself.data = data or {}self.queryset = querysetself.request = requestself.form_prefix = prefixself.filters = copy.deepcopy(self.base_filters)...@propertydef 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._formdef 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@propertydef qs(self):if not hasattr(self, '_qs'):qs = self.queryset.all()if self.is_bound:# ensure form validation before filteringself.errorsqs = self.filter_queryset(qs)self._qs = qsreturn 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:@classmethoddef get_filters(cls):"""Get all filters for the filterset. This is the combination of declared andgenerated filters."""# No model specified - skip filter generationif 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 generationif filter_name in cls.declared_filters:filters[filter_name] = cls.declared_filters[filter_name]continueif 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/tupleif 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' optionfilters.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 qsif self.distinct:qs = qs.distinct()lookup = '%s__%s' % (self.field_name, self.lookup_expr)qs = self.get_method(qs)(**{lookup: value})return qsclass CharFilter(Filter):field_class = forms.CharField

相信你看到这里,也就了解了django filter的整个来龙去脉。相信你也能自定义如下一个支持正则表达式的过滤器:

import django_filters class RegexFilter(filters.CharFilter):@classmethoddef valid(cls, value):try:re.compile(value)return Trueexcept re.error as e:raise Error('正则表达式语法错误')def filter(self, qs, value):self.valid(value)if value in EMPTY_VALUES:return qsif self.distinct:qs = qs.distinct()lookup = '%s__%s' % (self.field_name, self.lookup_expr)qs = self.get_method(qs)(**{lookup: value})return qsclass 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开头的产品

完活,收工。