DRF之排序類原始碼分析
【一】排序類介紹
- 在Django REST framework (DRF)中,排序類用於處理API端點的排序操作,允許客戶端請求按特定欄位對資料進行升序或降序排序。
- 排序類是一種特殊的過濾類
- DRF提供了內建的排序類,並且你也可以自定義排序類以滿足特定的需求。
【二】內建排序類OrderingFilter
rest_framework.filters.OrderingFilter
:這是DRF預設的排序類。
- 它允許客戶端在API請求中使用
?ordering=
引數來指定要排序的欄位。
- 例如,
?ordering=-created_at
將按 created_at
欄位降序排序。
【1】使用
from rest_framework.filters import OrderingFilter
class MyModelListView(ListAPIView):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_backends = [OrderingFilter] # 新增你的自定義排序類
ordering_fields = ['field1', 'field2'] # 定義允許排序的欄位
- 執行流程
- 當一個API請求到達時,Django REST framework將會執行檢視的
get_queryset
方法來獲取查詢集。
- 如果使用了
OrderingFilter
排序類,它會檢查請求中是否包含 ?ordering=
引數。
- 如果請求中包含
?ordering=
引數,OrderingFilter
會根據引數的值對查詢集進行排序。
- 排序後的查詢集將傳遞給檢視進行進一步處理和返回。
【2】原始碼分析
class OrderingFilter(BaseFilterBackend):
# The URL query parameter used for the ordering.
ordering_param = api_settings.ORDERING_PARAM
ordering_fields = None
ordering_title = _('Ordering')
ordering_description = _('Which field to use when ordering the results.')
template = 'rest_framework/filters/ordering.html'
# 獲取客戶端請求中的排序引數並返回排序順序
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.
"""
# 首先從請求引數中獲取排序引數(例如,?ordering=)
params = request.query_params.get(self.ordering_param)
if params:
# 然後將其拆分成欄位名
fields = [param.strip() for param in params.split(',')]
# 然後使用 remove_invalid_fields 方法來移除無效的欄位名
ordering = self.remove_invalid_fields(queryset, fields, view, request)
# 如果排序引數有效
if ordering:
# 將返回排序順序
return ordering
# No ordering was included, or all the ordering fields were invalid
# 如果沒有提供排序引數或所有欄位都無效,將呼叫 get_default_ordering 方法返回預設排序
return self.get_default_ordering(view)
# 獲取檢視的預設排序順序
def get_default_ordering(self, view):
# 如果檢視類中定義了 ordering 屬性
ordering = getattr(view, 'ordering', None)
if isinstance(ordering, str):
# 它將返回該屬性的值
return (ordering,)
# 否則將返回 None
return ordering
# 獲取預設允許排序的欄位
def get_default_valid_fields(self, queryset, view, context={}):
# If `ordering_fields` is not specified, then we determine a default
# based on the serializer class, if one exists on the view.
# 如果檢視有 get_serializer_class 方法
if hasattr(view, 'get_serializer_class'):
try:
# 嘗試從序列化器類中獲取欄位列表
serializer_class = view.get_serializer_class()
except AssertionError:
# Raised by the default implementation if
# no serializer_class was found
serializer_class = None
else:
serializer_class = getattr(view, 'serializer_class', None)
# 如果沒有序列化器類,將引發 ImproperlyConfigured 異常
if serializer_class is None:
msg = (
"Cannot use %s on a view which does not have either a "
"'serializer_class', an overriding 'get_serializer_class' "
"or 'ordering_fields' attribute."
)
raise ImproperlyConfigured(msg % self.__class__.__name__)
# 獲取到模型類的模型
model_class = queryset.model
# 校驗模型類中的欄位
model_property_names = [
# 'pk' is a property added in Django's Model class, however it is valid for ordering.
attr for attr in dir(model_class) if isinstance(getattr(model_class, attr), property) and attr != 'pk'
]
# 列出模型欄位和查詢註釋欄位
return [
(field.source.replace('.', '__') or field_name, field.label)
for field_name, field in serializer_class(context=context).fields.items()
if (
not getattr(field, 'write_only', False) and
not field.source == '*' and
field.source not in model_property_names
)
]
# 獲取允許排序的欄位
def get_valid_fields(self, queryset, view, context={}):
# 如果檢視定義了 ordering_fields,將返回該欄位,否則將根據 ordering_fields 的值執行不同的邏輯
valid_fields = getattr(view, 'ordering_fields', self.ordering_fields)
# 如果 ordering_fields 是 None
if valid_fields is None:
# Default to allowing filtering on serializer fields
# 將呼叫 get_default_valid_fields 方法獲取預設欄位列表
return self.get_default_valid_fields(queryset, view, context)
# 如果 ordering_fields 是 __all__
elif valid_fields == '__all__':
# View explicitly allows filtering on any model field
# 將允許對模型的所有欄位進行排序
valid_fields = [
(field.name, field.verbose_name) for field in queryset.model._meta.fields
]
valid_fields += [
(key, key.title().split('__'))
for key in queryset.query.annotations
]
else:
# 否則,將返回檢視中定義的排序欄位
valid_fields = [
(item, item) if isinstance(item, str) else item
for item in valid_fields
]
# 返回允許排序的欄位
return valid_fields
# 移除無效的排序欄位
def remove_invalid_fields(self, queryset, fields, view, request):
# 接受一個欄位列表,然後使用 get_valid_fields 方法獲取允許的欄位列表
valid_fields = [item[0] for item in self.get_valid_fields(queryset, view, {'request': request})]
# 校驗有效欄位的列表
def term_valid(term):
if term.startswith("-"):
term = term[1:]
return term in valid_fields
# 最後,它將返回一個僅包含有效欄位的列表
return [term for term in fields if term_valid(term)]
# 對查詢集進行排序
def filter_queryset(self, request, queryset, view):
# 呼叫 get_ordering 方法獲取排序順序
ordering = self.get_ordering(request, queryset, view)
if ordering:
# 然後使用 order_by 方法對查詢集進行排序
return queryset.order_by(*ordering)
# 如果沒有提供排序引數,將返回原始查詢集
return queryset
# 為HTML模板提供上下文資料
def get_template_context(self, request, queryset, view):
# 首先,它呼叫 get_ordering 方法獲取當前的排序順序(如果有的話),並將其儲存在變數 current 中
current = self.get_ordering(request, queryset, view)
# # 如果沒有排序順序,將設定 current 為 None
current = None if not current else current[0]
# 建立一個空列表 options,用於儲存可用的排序選項
options = []
# 建立一個上下文字典 context 包含以下鍵值對
context = {
# 儲存請求物件,以便在模板中訪問請求資訊
'request': request,
# 儲存當前排序狀態(可能為 None)
'current': current,
# 儲存排序引數名稱(例如,ordering)
'param': self.ordering_param,
}
# 迴圈遍歷可用的排序欄位
# 使用 get_valid_fields 方法獲取可用的排序欄位
for key, label in self.get_valid_fields(queryset, view, context):
# 將欄位名稱和升序排序標籤新增到 options 列表。
options.append((key, '%s - %s' % (label, _('ascending'))))
# 將欄位名稱加上 '-' 字首和降序排序標籤新增到 options 列表。
options.append(('-' + key, '%s - %s' % (label, _('descending'))))
# 將包含排序選項的列表新增到上下文字典中,以便在模板中訪問
context['options'] = options
# 返回一個包含有關當前排序狀態和可用排序選項的上下文字典
return context
# 生成HTML表示的排序控制元件
def to_html(self, request, queryset, view):
# 獲取渲染器
template = loader.get_template(self.template)
# 獲取上下文物件
context = self.get_template_context(request, queryset, view)
# 使用模板來渲染排序控制元件,並返回HTML程式碼
return template.render(context)
def get_schema_fields(self, view):
# 這是一個斷言語句,用於確保Core API庫已安裝。
# 如果未安裝,將引發AssertionError異常,提示使用者需要安裝Core API庫才能使用這個方法
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
# 用於確保Core Schema庫已安裝。
# 如果未安裝,將引發AssertionError異常,提示使用者需要安裝Core Schema庫才能使用這個方法。
assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
# 如果Core API和Core Schema庫都已安裝,該方法將返回一個包含排序引數描述資訊的列表。
return [
# 具體來說,它建立了一個coreapi.Field物件,該物件描述了排序引數的屬性
coreapi.Field(
# 定義引數的名稱,通常是ordering
name=self.ordering_param,
# 指示引數是否是必需的。在這裡,它設定為False,因為排序引數是可選的
required=False,
# 指示引數在請求的查詢字串中
location='query',
# 定義引數的架構。在這裡,它指定引數的型別為字串,並提供了標題和描述資訊,這些資訊將出現在API文件中
schema=coreschema.String(
title=force_str(self.ordering_title),
description=force_str(self.ordering_description)
)
)
]
# 返回一個包含描述排序引數的操作引數資訊的列表
def get_schema_operation_parameters(self, view):
return [
{
# 定義引數的名稱,通常是 ordering
'name': self.ordering_param,
# 指示引數是否是必需的。在這裡,它設定為 False,因為排序引數是可選的。
'required': False,
# 指示引數在請求中的位置。在這裡,它設定為 'query',表示排序引數位於查詢字串中。
'in': 'query',
# 提供有關引數的描述資訊,使用 force_str(self.ordering_description) 獲取排序引數的描述
'description': force_str(self.ordering_description),
# 定義引數的架構,包含有關引數型別的資訊。在這裡,它指定引數的型別為字串,表示排序引數的值應該是字串型別
'schema': {
'type': 'string',
},
},
]
【三】自定義排序類
【1】使用
from rest_framework.filters import OrderingFilter
class CustomOrderingFilter(OrderingFilter):
def get_ordering(self, request, queryset, view):
# 獲取客戶端傳遞的排序引數
ordering = request.query_params.get('ordering')
if ordering:
# 在此處可以根據自定義邏輯修改排序方式
return [ordering]
return super().get_ordering(request, queryset, view)
class MyModelListView(ListAPIView):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_backends = [CustomOrderingFilter] # 新增你的自定義排序類
ordering_fields = ['field1', 'field2'] # 定義允許排序的欄位
【2】分析
- 繼承 OrderingFilter
- 重寫 get_ordering 方法
- 自定義 過濾條件
- 將過濾後的檢視檢視集返回給檢視函式進一步呼叫