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 = 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源码大同小异,就不详细探讨了。

完活,收工。

你可能感兴趣的:(django,python,后端)