django——django_filters 使用与剖析

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

这样通过路由就可以访问了

django——django_filters 使用与剖析

源码剖析

首先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,所以要去找DjangoFilterBackendfilter_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
  1. get_filterset方法①:

    1. 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)
      
      1. get_filterset_class方法①.1

        1. 这个方法内容有点多,简言之

        2. # 拿到自定义的PriceOrderModelFilter
          filterset_class = getattr(view, 'filter_class', None)  
          
        3. # 拿到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
          
      2. 这样get_filterset_class方法就拿到了PriceOrderModelFilter

      3. # 这一行就不解释了
        if filterset_class is None:
            return None
        
      4. 然后执行get_filterset_kwargs方法

        1. # 封装请求参数
          def get_filterset_kwargs(self, request, queryset, view):
              return {
                  'data': request.query_params,
                  'queryset': queryset,
                  'request': request,
              }
          
        2. data就是筛选的请求url

        3. <QueryDict: {'type': ['exists'], 'state': ['discussion'], 'company': [''], 'product': [''], 'brand': [''], 'start_date': ['2021-5-1'], 'end_date': ['2021-5-20']}>
          
        4. queryset就是还没有被筛选的数据集

        5. request就是restframework再封装的请求

      5. 最后拿到实例化后的PriceOrderModelFilter对象

    2. 拿到PriceOrderModelFilter对象②

# 没有filter实例对象就直接返回所有的数据  ②
if filterset is None:
  	return queryset
# 这一步内容比较多,只挑部分有意义的讲
if not filterset.is_valid() and self.raise_exception:  # ③
  1. 大概流程就是会将PriceOrderModelFilter里面的内容转化成django的form类

  2. 通过django的form校验,对url请求的参数进行校验

  3. 有错误就报错,没有就往下走

  4. 讲一下将PriceOrderModelFilter的内容转化为form的细节

    1. # 会对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'] 
    
  5. 最后执行filterset.qs

    1. # 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
      
    2. 重点来看filter_queryset方法

      1. # 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
        
      2. 去前面转化好的form里面,获取cleaned_data,然后开始遍历

      3. 通过遍历,拿到每一个django_filters.Filed,再执行filter方法

      4. 这里的filter方法

        1. # 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
          
        2. # ✨✨✨
          self.field_name = field_name
          self.lookup_expr = lookup_expr
          
        3. # 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
          
        4. (**{lookup: value})如果看不明白的话,可以点击参考

      5. 这样就把可筛选的数据集筛选出来了

  6. 建议搜索的字段别太多,每一次的filter,都是一个数据库访问,会影响性能。

创作不易,转载请标明出处和附带链接

上一篇:python-小知识点总结


下一篇:django 拼接多个 queryset 并按顺序排列