django_filters 使用与剖析
默认已经配置好环境
使用
模型如下:
# models.py
class PriceOrderModel(ModelBase):
"""询价单"""
code = models.CharField("编号", max_length=256, null=True, blank=True, unique=True)
state = models.CharField("状态", max_length=20, choices=[
("discussion", "洽谈中"),("cancel", "取消"),("ordered", "已转订单")
], default='discussion')
date = models.DateField(default=timezone.localdate, verbose_name='日期')
volume = models.FloatField("体积", null=True, blank=True)
amount = models.FloatField("金额", null=True, blank=True)
total = models.IntegerField("条目数", null=True, blank=True)
create_user = models.ForeignKey(Users, on_delete=models.CASCADE, related_name="price_orders", verbose_name="创建人", null=True, blank=True)
过滤类如下:
# my_filter.py
import django_filters
from order.models import PriceOrderModel
class PriceOrderModelFilter(django_filters.FilterSet):
company = django_filters.CharFilter(field_name='create_user__company', lookup_expr='icontains', label='客户')
product = django_filters.CharFilter(method='search_cargos', label='品名')
brand = django_filters.CharFilter(method='search_brand', label='品牌')
start_date = django_filters.DateFilter(field_name='date', lookup_expr='gte', label='开始时间') # 大于等于
end_date = django_filters.DateFilter(field_name='date', lookup_expr='lte', label='结束时间') # 小于等于
def search_brand(self, queryset, field, value):
# do somethings
return queryset.filter(xxxxxx)
class Meta:
model = PriceOrderModel
fields = ['type', 'state']
视图类:
# views.py
from rest_framework import generics
from django_filters.rest_framework import DjangoFilterBackend
class PriceOrderListView(generics.ListCreateAPIView):
permission_classes = (IsAuthenticated, IsActivePermission)
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
queryset = PriceOrderModel.objects.all()
serializer_class = PriceOrderModelSerializer
filter_backends = [DjangoFilterBackend]
filter_class = PriceOrderModelFilter
这样通过路由就可以访问了
源码剖析
首先ListCreateAPIView
继承了如下:
class ListCreateAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin,
GenericAPIView):
在GenericAPIView
类中,实现了一个叫做filter_queryset
的方法
class GenericAPIView(views.APIView):
# .....
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
这个方法会根据你定义的filter_backends
,去执行filter_queryset
方法
filter_backends
定义的是DjangoFilterBackend
,所以要去找DjangoFilterBackend
的filter_queryset
方法
class DjangoFilterBackend(metaclass=RenameAttributes):
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
-
get_filterset
方法①:-
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)
-
get_filterset_class
方法①.1-
这个方法内容有点多,简言之
-
# 拿到自定义的PriceOrderModelFilter filterset_class = getattr(view, 'filter_class', None)
-
# 拿到PriceOrderModelFilter,在queryset有数据集的时候,返回PriceOrderModelFilter if filterset_class: filterset_model = filterset_class._meta.model # FilterSets do not need to specify a Meta class if filterset_model and queryset is not None: assert issubclass(queryset.model, filterset_model), \ 'FilterSet model %s does not match queryset model %s' % \ (filterset_model, queryset.model) return filterset_class # 。。。。。 return None
-
-
这样
get_filterset_class
方法就拿到了PriceOrderModelFilter
-
# 这一行就不解释了 if filterset_class is None: return None
-
然后执行
get_filterset_kwargs
方法-
# 封装请求参数 def get_filterset_kwargs(self, request, queryset, view): return { 'data': request.query_params, 'queryset': queryset, 'request': request, }
-
data就是筛选的请求url
-
<QueryDict: {'type': ['exists'], 'state': ['discussion'], 'company': [''], 'product': [''], 'brand': [''], 'start_date': ['2021-5-1'], 'end_date': ['2021-5-20']}>
-
queryset就是还没有被筛选的数据集
-
request就是restframework再封装的请求
-
-
最后拿到实例化后的
PriceOrderModelFilter
对象
-
-
拿到
PriceOrderModelFilter
对象②
-
# 没有filter实例对象就直接返回所有的数据 ②
if filterset is None:
return queryset
# 这一步内容比较多,只挑部分有意义的讲
if not filterset.is_valid() and self.raise_exception: # ③
-
大概流程就是会将PriceOrderModelFilter里面的内容转化成django的form类
-
通过django的form校验,对url请求的参数进行校验
-
有错误就报错,没有就往下走
-
讲一下将PriceOrderModelFilter的内容转化为form的细节
-
# 会对url的customer键,对模型的客户公司(create_user__company)进行搜索,按照`icontains`(忽略大小写的包含搜索,也就是忽略大小写的模糊搜索)搜索 customer = django_filters.CharFilter(field_name='customer__company', lookup_expr='icontains', label='客户') # 自定义方法,最后返回对应模型的queryset数据集就行 product = django_filters.CharFilter(method='search_cargos', label='品名') # 对date字段,进行范围搜索gte开始时间lte结束时间 start_date = django_filters.DateFilter(field_name='date', lookup_expr='gte', label='开始时间') end_date = django_filters.DateFilter(field_name='date', lookup_expr='lte', label='结束时间')
# Meta下的fields,这里定义的字段,会进行精准搜索 class Meta: model = PriceOrderModel fields = ['type', 'state']
-
-
最后执行
filterset.qs
-
# django_filters/restframework/backends.py # _qs防止重复搜索 @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
-
重点来看
filter_queryset
方法-
# django_filters/filterset.py def filter_queryset(self, queryset): """ Filter the queryset with the underlying form's `cleaned_data`. You must call `is_valid()` or `errors` before calling this method. This method should be overridden if additional filtering needs to be applied to the queryset before it is cached. """ 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
-
去前面转化好的form里面,获取
cleaned_data
,然后开始遍历 -
通过遍历,拿到每一个
django_filters.Filed
,再执行filter
方法 -
这里的
filter
方法-
# django_filters/filters.py 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
-
# ✨✨✨ self.field_name = field_name self.lookup_expr = lookup_expr
-
# django_filters/filters.py # ⭕️看是要过滤还是排除 def get_method(self, qs): """Return filter method based on whether we're excluding or simply filtering. """ return qs.exclude if self.exclude else qs.filter
-
(**{lookup: value})
如果看不明白的话,可以点击参考
-
-
这样就把可筛选的数据集筛选出来了
-
-
-
建议搜索的字段别太多,每一次的filter,都是一个数据库访问,会影响性能。