排序源码分析
ListAPIView是视图家族的工具视图类,因为继承了ListModelMixin类,所以有了list群查方法。而排序就是在这个list方法里面进行的。
ListModelMixin
class ListModelMixin: """ List a queryset. """ def list(self, request, *args, **kwargs): #看名字也知道是这一步完成的过滤 queryset = self.filter_queryset(self.get_queryset()) #这一步完成的是获取分页的页数 page = self.paginate_queryset(queryset) #如果没有分页直接做返回,如果有的话就在这里面再做处理 if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data)
这里的filter_queryset点不过去,所以如果我们想要给自己的类加上过滤条件,就要进入GenericAPIView(ListAPIView也继承了这个),发现了一个类属性filter_backends = api_settings.DEFAULT_FILTER_BACKENDS ,表示在filter_backends 里有一些类,类里面有一些方法,直接定位,找到一个filters.py
进入filters.py文件,这个文件里有一个排序类OrderingFilter,一个搜索类SearchFilter,和一个基类BaseFilterBackend,排序类和搜索类继承这个基类,并且三个类都有 filter_queryset 方法,这就很明显要我们继承这个类重写这个方法就好了。
先分析一下OrderingFilter的 filter_queryset方法,发现他是传进去一个queryset,返回一个queryset,那么肯定是在这里面给过滤了,在这里面把他的数据量变少就好了。怎么做的?queryset有一个特点,那就是他可以继续使用queryset的方法,可以继续加filter!
看一下 OrderingFilter 的filter_queryset 是怎么写的
def filter_queryset(self, request, queryset, view): ordering = self.get_ordering(request, queryset, view) if ordering: #这一句就完成了排序,所以就看ordering是怎么拿到的 return queryset.order_by(*ordering) return queryset
看一下get_ordering拿到的是什么东西赋值给 ordering
def get_ordering(self, request, queryset, view): """ Ordering is set by a comma delimited ?ordering=... query parameter. The `ordering` query parameter can be overridden by setting the `ordering_param` value on the OrderingFilter or by specifying an `ORDERING_PARAM` value in the API settings. """ #query_params表示前端发送的请求所带的参数,想要在这个参数里面获取ordering_param表示的值,所以去看要去找到ordering_param这个属性值,结果在配置文件中找到了'ORDERING_PARAM': 'ordering',所以这里的ordering_param代表的就是ordering,我们只需要在请求中附带参数,用“ordering=xxx”可以了。 params = request.query_params.get(self.ordering_param) if params: #这里表示请求中可以传多个排序条件,通过逗号 fields = [param.strip() for param in params.split(',')] #为了防止我们乱传一些ordering=asdfasdfa这样的东西,这个方法实现了排除不符合条件的排序条件,进入这个方法看一下是怎么做的 ordering = self.remove_invalid_fields(queryset, fields, view, request) if ordering: return ordering # No ordering was included, or all the ordering fields were invalid return self.get_default_ordering(view)
remove_invalid_fields
def remove_invalid_fields(self, queryset, fields, view, request): #用这个方法来获取允许的排序条件。 valid_fields = [item[0] for item in self.get_valid_fields(queryset, view, {'request': request})] return [term for term in fields if term.lstrip('-') in valid_fields and ORDER_PATTERN.match(term)]
进入get_valid_fields方法
def get_valid_fields(self, queryset, view, context={}): #通过反射从我们自己的视图类中获取'ordering_fields' valid_fields = getattr(view, 'ordering_fields', self.ordering_fields)
所以很明白了,我们需要在自己的视图类中配置 ordering_fields=[ ],比如id ,price等等,这样请求参数传的不符合的数据就会被抛掉。
比如:
from django_filters.rest_framework import DjangoFilterBackend from .filters import CourseFilterSet from .filters import LimitFilter class FreeCourseListAPIView(ListAPIView): queryset = models.Course.objects.filter(is_delete=False, is_show=True).order_by('-orders').all() serializer_class = serializers.FreeCourseModelSerializer # 配置过滤器类 filter_backends = [OrderingFilter, LimitFilter] # LimitFilter自定义过滤器 # 参与排序的字段: ordering=-price,id ordering_fields = ['price', 'id']
filters.py
from rest_framework.filters import BaseFilterBackend class LimitFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): limit = request.query_params.get('limit') try: return queryset[:int(limit)] except: return queryset
搜索源码分析
和上面的一样,从SearchFilter进去找,
看到这个方法
def get_search_fields(self, view, request): """ Search fields are obtained from the view, but the request is always passed to this method. Sub-classes can override this method to dynamically change the search fields based on request content. """ #从视图里面拿search_fields return getattr(view, 'search_fields', None)
所以只要在视图类里配置这个search_fields就可以了。
class FreeCourseListAPIView(ListAPIView): queryset = models.Course.objects.filter(is_delete=False, is_show=True).order_by('-orders').all() serializer_class = serializers.FreeCourseModelSerializer # 配置过滤器类 # filter_backends = [OrderingFilter, LimitFilter] # LimitFilter自定义过滤器 filter_backends = [OrderingFilter, SearchFilter, DjangoFilterBackend] # 参与排序的字段: ordering=-price,id ordering_fields = ['price', 'id', 'students'] # 参与搜索的字段: search=python (name或brief字段中带python就ok) search_fields = ['name', 'brief']
分页器源码分析
class ListModelMixin: """ List a queryset. """ def list(self, request, *args, **kwargs): #看名字也知道是这一步完成的过滤 queryset = self.filter_queryset(self.get_queryset()) #这一步完成的是获取分页的页数 page = self.paginate_queryset(queryset) #如果没有分页直接做返回,如果有的话就在这里面再做处理 if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data)
直接去找到pagination.py文件
里面有四个类, BasePagination基类,CursorPagination根据游标分类 , LimitOffsetPagination根据偏移分页 ,PageNumberPagination基础分页。看一下PageNumberPagination,有一些配置,我们只需要设置这些配置就可以了。
page_size = api_settings.PAGE_SIZE django_paginator_class = DjangoPaginator # Client can control the page using this query parameter. page_query_param = 'page' page_query_description = _('A page number within the paginated result set.') # Client can control the page size using this query parameter. # Default is 'None'. Set to eg 'page_size' to enable usage. page_size_query_param = None page_size_query_description = _('Number of results to return per page.') # Set to an integer to limit the maximum page size the client may request. # Only relevant if 'page_size_query_param' has also been set. max_page_size = None
有经验了,知道一定是在视图类里面配置。
views
from .paginations import CoursePageNumberPagination, CourseLimitOffsetPagination, CourseCursorPagination # 分类筛选:django-filter:filter_backends配置DjangoFilterBackend,再在filter_fields中配置分组筛选的字段 from django_filters.rest_framework import DjangoFilterBackend from .filters import CourseFilterSet class FreeCourseListAPIView(ListAPIView): queryset = models.Course.objects.filter(is_delete=False, is_show=True).order_by('-orders').all() serializer_clas s = serializers.FreeCourseModelSerializer # 配置过滤器类 # filter_backends = [OrderingFilter, LimitFilter] # LimitFilter自定义过滤器 filter_backends = [OrderingFilter, SearchFilter, DjangoFilterBackend] # 参与排序的字段: ordering=-price,id ordering_fields = ['price', 'id', 'students'] # 参与搜索的字段: search=python (name字段中带python就ok) search_fields = ['name', 'brief'] # 分页器 pagination_class = CoursePageNumberPagination # pagination_class = CourseLimitOffsetPagination # pagination_class = CourseCursorPagination
分页器Pagination
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination class CoursePageNumberPagination(PageNumberPagination): # 默认一页条数 page_size = 2 # 选择哪一页的key page_query_param = 'page' # 用户自定义一页条数 page_size_query_param = 'page_size' # 用户自定义一页最大控制条数 max_page_size = 10 class CourseLimitOffsetPagination(LimitOffsetPagination): # 默认一页条数 default_limit = 2 # 从offset开始往后显示limit条 limit_query_param = 'limit' offset_query_param = 'offset' max_limit = 2 class CourseCursorPagination(CursorPagination): cursor_query_param = 'cursor' page_size = 2 page_size_query_param = 'page_size' max_page_size = 2 # ordering = 'id' # 默认排序规则,不能和排序过滤器OrderingFilter共存
注意:分页后的结果和没有分页的结果是不一样的,分页后返回的是字典,有下一页的路由,上一页的路由,和result,分页后的结果是把没有分页的结果丢进result返回的
分类筛选过滤器
普通使用
分类筛选 drf 和 django 都干不了,需要安装一个插件
pip install django-filters
插个插件可以解决前后端不分离的分类筛选,也可以完成前后端分离的筛选
用法:
from .paginations import CoursePageNumberPagination, CourseLimitOffsetPagination, CourseCursorPagination # 分类筛选:django-filter:filter_backends配置DjangoFilterBackend,再在filter_fields中配置分组筛选的字段 from django_filters.rest_framework import DjangoFilterBackend from .filters import CourseFilterSet class FreeCourseListAPIView(ListAPIView): queryset = models.Course.objects.filter(is_delete=False, is_show=True).order_by('-orders').all() serializer_clas s = serializers.FreeCourseModelSerializer # 配置过滤器类 # filter_backends = [OrderingFilter, LimitFilter] # LimitFilter自定义过滤器 filter_backends = [OrderingFilter, SearchFilter, DjangoFilterBackend] # 参与排序的字段: ordering=-price,id ordering_fields = ['price', 'id', 'students'] # 参与搜索的字段: search=python (name字段中带python就ok) search_fields = ['name', 'brief'] # 参与分类筛选的字段:所有字段都可以,但是用于分组的字段更有意义 filter_fields = ['course_category']
分类筛选:django-filter:filter_backends配置DjangoFilterBackend,再在filter_fields中配置分组筛选的字段,就完成了分类筛选的基本使用
高级使用
区间筛选过滤器
我们说过的,过滤器就是把数据给减少了返回,所以他一定是走了 filter_queryset 方法的,看一下 django-filter 里面,找到这个方法
进入DjangoFilterBackend,找到 django-filter
def filter_queryset(self, request, queryset, view): filterset = self.get_filterset(request, queryset, view) #如果为空,就直接返回原来的queryset,就等于啥也没过滤 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方法,是怎么获取filterset的
def get_filterset(self, request, queryset, view): #这里一定不为空,不然的话就返回none了,然后前面那个方法就原样返回queryset了,等于什么也没做。所以进去看一下get_filterset_class方法 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
def get_filterset_class(self, view, queryset=None): """ Return the `FilterSet` class used to filter the queryset. """ #从视图类中找filterset_class和filterset_fields,我们在视图类中配置的是filter_fields,所以这两个都是none filterset_class = getattr(view, 'filterset_class', None) filterset_fields = getattr(view, 'filterset_fields', None) # TODO: remove assertion in 2.1 #这里又判断了,如果filterset_class是空,有filter_class也行,但是这个也没有 if filterset_class is None and hasattr(view, 'filter_class'): utils.deprecate( "`%s.filter_class` attribute should be renamed `filterset_class`." % view.__class__.__name__) filterset_class = getattr(view, 'filter_class', None) # TODO: remove assertion in 2.1 #这里才是我们能进去的地方 if filterset_fields is None and hasattr(view, 'filter_fields'): utils.deprecate( "`%s.filter_fields` attribute should be renamed `filterset_fields`." % view.__class__.__name__) #把我们在视图类里配置的filter_fields反射出来给了filterset_fields filterset_fields = getattr(view, 'filter_fields', None) #空,不走 “1” 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 #满足,走这个 if filterset_fields and queryset is not None: #看一下这个filterset_base,ctrl+左键一点,跑上面找到了一个filterset_base = filterset.FilterSet,然后看一下他导入的filterset,是从哪里导入的,结果是 from . import filters, filterset,这个 . ,看一下目录,他代表的就是django_filters下的rest_framwork,那么filterset就找到了 MetaBase = getattr(self.filterset_base, 'Meta', object) #所以这里的AutoFilterSet类就相当于继承了filterset.FilterSet “2” class AutoFilterSet(self.filterset_base): class Meta(MetaBase): #这里能找到他的类是什么 model = queryset.model fi elds = filterset_fields return AutoFilterSet return None
所以看上面的“1”处,如果你给的是类,他就返回一个类,如果你给的是一个字段,他也
把你格式化成类返回了(看“2”),所以我们把这个AutoFilterSet拉出来,放在自己写的过滤器里就好了
filters
# 基于django-filter插件,完成指定区间筛选(一般都是对应数字字段) from django_filters.rest_framework.filterset import FilterSet from . import models #这个名字可以改 class CourseFilterSet(FilterSet): class Meta: model = models.Course fields = ['course_category']
写完后,前面的普通使用的filter_fields = ['course_category']也可以写成filterset_class=CourseFilterSet
问题:这样好像变得更麻烦了,我为什么不直接写个字段就好呢?
答案:因为后面的方式我们可以自定义字段,比如实现区间筛选。
from django_filters.rest_framework.filterset import FilterSet from django_filters import filters from . import models class CourseFilterSet(FilterSet): #field_name表示我这个自定义字段跟price这个字段有关系,lookup_expr表示筛选规则,其实这个内部就是做了基于双下划綫的规则 max_price = filters.NumberFilter(field_name='price', lookup_expr='lte') min_price = filters.NumberFilter(field_name='price', lookup_expr='gte') class Meta: model = models.Course fields = ['course_category', 'max_price', 'min_price']