DRF之過濾類原始碼分析

ssrheart發表於2024-04-23

DRF之過濾類原始碼分析

【一】過濾類介紹及BaseFilterBackend

  • Django REST framework(DRF)中的過濾類允許你在API檢視中對查詢進行過濾,以根據特定條件篩選結果集。
  • 過濾類是DRF的一部分,它允許你輕鬆地新增各種過濾選項,以滿足不同用例的需求。
class BaseFilterBackend:
    """
    A base class from which all filter backend classes should inherit.
    """

    def filter_queryset(self, request, queryset, view):
        """
        Return a filtered queryset.
        """
        raise NotImplementedError(".filter_queryset() must be overridden.")

    def get_schema_fields(self, view):
        assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
        assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
        return []

    def get_schema_operation_parameters(self, view):
        return []

【二】內建過濾類SearchFilter

【1】使用

# 過濾:必須繼承 GenericAPIView 及其子類,才能使用(如果繼承APIView,則不能這麼寫)
# DRF 給我們提供了一個排序類
from rest_framework.filters import SearchFilter


class BookView(GenericViewSet, ListModelMixin):
    queryset = models.Book.objects.all()
    serializer_class = BookSerializer
    filter_backends = [SearchFilter]  # 排序類
    # SearchFilter:必須配合一個類屬性
    # 按照那些欄位進行篩選
    # http://127.0.0.1:8000/app01/v1/books/?search=夢
    # 只要name/price中帶 夢 都能被搜出來
    search_fields = ['name']

【2】原始碼分析

class SearchFilter(BaseFilterBackend):
    # The URL query parameter used for the search.
    
    # 搜尋引數的名稱,預設為 api_settings.SEARCH_PARAM,通常是 search。
    # 拼在路由尾部的關鍵字  http://127.0.0.1:8000/app01/v1/books/?search=夢
    # 你可以透過在API檢視中自定義該引數來更改搜尋引數的名稱。
    search_param = api_settings.SEARCH_PARAM
    
    # 用於HTML渲染搜尋部件的模板路徑,預設為 'rest_framework/filters/search.html'。
    template = 'rest_framework/filters/search.html'
    
    # 定義了搜尋條件的字首和它們對應的查詢操作。
    # 例如,^ 表示 "istartswith"(不區分大小寫的開始於)
    # = 表示 "iexact"(不區分大小寫的精確匹配) 使用居多
    # @ 表示 "search"(全文搜尋)
    # $ 表示 "iregex"(不區分大小寫的正規表示式匹配)。
    lookup_prefixes = {
        '^': 'istartswith',
        '=': 'iexact',
        '@': 'search',
        '$': 'iregex',
    }
    
    # 
    search_title = _('Search')
    
    # 
    search_description = _('A search term.')
	
    # 從檢視中獲取搜尋欄位列表
    # 可以覆蓋此方法以動態更改搜尋欄位
    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)
	
    # 解析查詢引數中的搜尋條件,將其分割成一個列表。
    def get_search_terms(self, request):
        """
        Search terms are set by a ?search=... query parameter,
        and may be comma and/or whitespace delimited.
        """
        # 獲取到路由尾部攜帶的引數
        params = request.query_params.get(self.search_param, '')
        
        # 空格代替 空
        params = params.replace('\x00', '')  # strip null characters
        # 多欄位進行切分
        params = params.replace(',', ' ')
        # 返回切分後的引數
        return params.split()
	
    # 構建搜尋條件,根據字首選擇合適的查詢操作。
    # field_name 引數是傳遞給過濾器的搜尋欄位名,可能包含字首
    def construct_search(self, field_name):
        
        #  字典定義了搜尋條件的字首和它們對應的查詢操作。
        # 檢查 field_name 的第一個字元(字首),並使用 get 方法從 lookup_prefixes 字典中獲取對應的查詢操作。
        lookup = self.lookup_prefixes.get(field_name[0])
        
        # 如果字首存在
        if lookup:
            # 則將字首從 field_name 中去除,並將查詢操作儲存在 lookup 變數中。
            field_name = field_name[1:]
        else:
            # 如果字首不存在,則預設使用 'icontains' 查詢操作,這表示執行不區分大小寫的部分匹配。
            lookup = 'icontains'
        
        # 使用 LOOKUP_SEP 連線欄位名和查詢操作,返回最終的搜尋條件字串。
        # LOOKUP_SEP 是Django中用於連線查詢欄位和操作的分隔符,通常是雙下劃線 __。
		# 例如,如果傳遞的搜尋欄位名是 ^name,那麼這個方法將返回 "name__istartswith",這表示要在 name 欄位上執行不區分大小寫的開始於匹配操作。
        return LOOKUP_SEP.join([field_name, lookup])
	
    # 返回一個布林值,指示是否應該使用 distinct() 方法來查詢結果集以避免重複項。
    # queryset 引數是要進行過濾的查詢集,通常是資料庫查詢的結果集。
	# search_fields 引數是用於搜尋的欄位列表,這些欄位可能包含字首。
    def must_call_distinct(self, queryset, search_fields):
        """
        Return True if 'distinct()' should be used to query the given lookups.
        """
        
        # 迭代 search_fields 中的每個搜尋欄位
        for search_field in search_fields:
            
            # 對於每個搜尋欄位,它獲取與查詢集相關聯的模型的後設資料(opts = queryset.model._meta)
            opts = queryset.model._meta
            
            # 檢查搜尋欄位的第一個字元是否在 lookup_prefixes 中
            if search_field[0] in self.lookup_prefixes:
                # 如果字首存在,則將字首從搜尋欄位中去除(search_field = search_field[1:])。
                search_field = search_field[1:]
            # Annotated fields do not need to be distinct
            
            # 檢查搜尋欄位是否已經在查詢集的註釋中。
            if isinstance(queryset, models.QuerySet) and search_field in queryset.query.annotations:
                # 如果欄位已經在註釋中,說明該欄位已經被標記為註釋欄位,通常不需要使用 distinct()。
                continue
                
            # 如果欄位不在註釋中,則進一步檢查欄位是否包含巢狀關係(例如,related_field__nested_field)。
            parts = search_field.split(LOOKUP_SEP)
            
            # 如果欄位是巢狀關係,它會更新模型的後設資料以跟蹤關係路徑,並檢查是否存在多對多關係(m2m 關係)。
            for part in parts:
                
                field = opts.get_field(part)
                if hasattr(field, 'get_path_info'):
                    # This field is a relation, update opts to follow the relation
                    path_info = field.get_path_info()
                    opts = path_info[-1].to_opts
                    
                    # # 如果欄位是多對多關係,就需要呼叫 distinct() 方法,因為多對多關係通常會導致結果集中的重複項。
                    if any(path.m2m for path in path_info):
                        # This field is a m2m relation so we know we need to call distinct
                        # 如果需要呼叫 distinct(),它將返回 True
                        return True
                else:
                    # This field has a custom __ query transform but is not a relational field.
                    break
                    
        # 最後,方法返回一個布林值,指示是否應該呼叫 distinct()。
        # 如果需要呼叫 distinct(),它將返回 True,否則返回 False。
        return False
	
    # 實際的查詢過濾操作,根據查詢引數中的搜尋條件修改查詢集
    def filter_queryset(self, request, queryset, view):
        # 獲取了檢視中定義的搜尋欄位列表(search_fields)和查詢引數中傳遞的搜尋條件(search_terms)。
        search_fields = self.get_search_fields(view, request)
        search_terms = self.get_search_terms(request)
		
        # 如果沒有定義搜尋欄位或者沒有傳遞搜尋條件
        if not search_fields or not search_terms:
            # 就直接返回原始的查詢集 queryset,不進行任何過濾。
            return queryset
		
        # 構建了一個包含了所有搜尋欄位的ORM查詢操作列表 orm_lookups
        # 透過呼叫 construct_search 方法將搜尋欄位名轉換為相應的查詢操作。
        orm_lookups = [
            self.construct_search(str(search_field))
            for search_field in search_fields
        ]
		
        # 始化了兩個變數,base 和 conditions。
        # base 是原始的查詢集
        # conditions 是用來儲存過濾條件的列表。
        base = queryset
        conditions = []
        
        # 迭代每個搜尋條件(search_term)和每個查詢操作(orm_lookup),並建立一個 Q 物件,將查詢操作和搜尋條件傳遞給它。
        # 這個 Q 物件表示了一個或多個查詢條件的邏輯或關係。
        for search_term in search_terms:
            
            # 建立的 Q 物件列表儲存在 queries 中
            queries = [
                models.Q(**{orm_lookup: search_term})
                for orm_lookup in orm_lookups
            ]
            
            # 然後使用 reduce(operator.or_, queries) 將它們組合成一個包含所有查詢條件的 Q 物件。
            # 最終,conditions 列表包含了一個或多個 Q 物件,每個 Q 物件代表一個搜尋條件的查詢操作。
            # 使用 reduce(operator.and_, conditions) 將所有的 Q 物件組合成一個包含所有搜尋條件的查詢操作,這個查詢操作表示了所有搜尋條件之間的邏輯與關係。
            conditions.append(reduce(operator.or_, queries))
            
        

		# 最後,使用 queryset.filter() 方法,將這個複合查詢操作應用於原始查詢集,以便執行過濾操作。
        queryset = queryset.filter(reduce(operator.and_, conditions))
        
		# 如果必須呼叫 distinct() 方法(透過呼叫 must_call_distinct 方法來確定)
        if self.must_call_distinct(queryset, search_fields):
            # Filtering against a many-to-many field requires us to
            # call queryset.distinct() in order to avoid duplicate items
            # in the resulting queryset.
            # We try to avoid this if possible, for performance reasons.
            
            # 在過濾後的查詢集上呼叫 distinct() 以確保結果集不包含重複項
            queryset = distinct(queryset, base)
            
        # 返回過濾後的資料集
        return queryset
	
    # 用於生成搜尋框的HTML表示形式,以便在API的瀏覽器瀏覽介面中顯示
    def to_html(self, request, queryset, view):
        '''
        目的是生成一個包含搜尋框的HTML表示,以便在API的瀏覽器瀏覽介面中讓使用者輸入搜尋條件。
        搜尋框的樣式和佈局通常由指定的模板檔案定義,這使得可以根據需要自定義搜尋框的外觀。
        搜尋引數的名稱和搜尋條件的值也被傳遞到模板中,以便在HTML中動態生成搜尋框。
        '''
        
        # 檢查檢視是否定義了搜尋欄位列表(search_fields)。
        if not getattr(view, 'search_fields', None):
            # 如果沒有定義搜尋欄位,就直接返回空字串,表示不需要顯示搜尋框。
            return ''
		
        # 獲取查詢引數中傳遞的搜尋條件(search_terms)。
        term = self.get_search_terms(request)
        
        # 如果搜尋條件存在,就將第一個搜尋條件(通常只支援一個搜尋條件)儲存在 term 變數中。
        term = term[0] if term else ''
        
        # 建立一個名為 context 的字典,其中包含兩個鍵值對:
        # 'param':用於指定搜尋引數的名稱,通常是 search。
        # 'term':包含了搜尋條件的值,即使用者在搜尋框中輸入的文字。
        context = {
            'param': self.search_param,
            'term': term
        }
        
        # 使用 Django 的 loader.get_template 函式獲取指定模板檔案(self.template)的模板物件。
        # 這個模板通常包含了搜尋框的HTML表示。
        template = loader.get_template(self.template)
        
        # 使用 template.render(context) 渲染模板,將 context 字典中的資料傳遞給模板,生成包含搜尋框的HTML表示
        return template.render(context)

    def get_schema_fields(self, view):
        '''
        目的是生成一個包含搜尋引數的欄位定義,以便在API文件中顯示搜尋引數的相關資訊。
        這有助於API使用者理解如何使用搜尋功能,並構建正確的搜尋請求。
        '''
        
        # 透過 assert 語句確保 coreapi 和 coreschema 這兩個庫已經安裝。
        # 這些庫是用於構建API文件和描述API資料結構的工具。
        assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
        assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
        
        # 建立一個列表,其中包含一個 CoreAPI 欄位定義物件。
        # 這個欄位定義物件用於描述搜尋引數。
        # 返回一個包含了搜尋引數欄位定義的列表。這個列表通常會與其他欄位定義一起用於生成API文件,並且可以讓API客戶端了解如何構建搜尋請求
        return [
            coreapi.Field(
                # name:搜尋引數的名稱,通常是 search。
                name=self.search_param,
                
                # required:指定搜尋引數是否是必需的,這裡設定為 False 表示不是必需的。
                required=False,
                
                # location:指定搜尋引數在請求中的位置,這裡設定為 query,表示在查詢字串中。
                location='query',
                
                # schema:定義搜尋引數的資料模式,這裡使用 coreschema.String 來定義搜尋引數的資料型別為字串,並提供了標題(title)和描述(description)。
                schema=coreschema.String(
                    title=force_str(self.search_title),
                    description=force_str(self.search_description)
                )
            )
        ]
	
    # 用於生成API操作的引數定義,這些引數用於API文件的生成
    # 返回包含了引數定義的列表。這個列表通常會與API操作一起用於生成API文件,以幫助API使用者瞭解如何構建請求和使用引數。
    def get_schema_operation_parameters(self, view):
        '''
        API文件中顯示操作的引數資訊。這有助於API使用者理解如何構建API請求,並知道哪些引數是可選的、哪些是必需的
        '''
        
        # 建立了一個包含引數定義的列表。每個引數定義都是一個字典,描述了一個API操作的引數。
        return [
            {
                # 'name':指定引數的名稱,通常是 search。
                'name': self.search_param,
                
                # 'required':指定引數是否是必需的,這裡設定為 False 表示不是必需的。
                'required': False,
                
                # 'in':指定引數在請求中的位置,這裡設定為 query,表示在查詢字串中。
                'in': 'query',
                
                # 'description':提供引數的描述,使用了 force_str 函式來確保描述是字串型別。
                'description': force_str(self.search_description),
                
                # 'schema':定義引數的資料模式,這裡指定引數的資料型別為字串('type': 'string')。
                'schema': {
                    'type': 'string',
                },
            },
        ]

【三】第三方過濾類DjangoFilterBackend

【1】使用

# (2) 過濾:必須繼承 GenericAPIView 及其子類,才能使用(如果繼承APIView,則不能這麼寫)
# 第三方過濾類: pip3.9 install django-filter
from django_filters.rest_framework import DjangoFilterBackend


class BookView(GenericViewSet, ListModelMixin):
    queryset = models.Book.objects.all()
    serializer_class = BookSerializer
    filter_backends = [DjangoFilterBackend]  # 排序類
    # SearchFilter:必須配合一個類屬性
    # 按照那些欄位進行篩選
    # http://127.0.0.1:8000/app01/v1/books/?search=夢
    # 根據指定欄位進行篩選,按名字和價格精準匹配
    filterset_fields = ['name', 'price']

【2】原始碼分析

class DjangoFilterBackend:
    # 用於指定用於過濾的過濾器集的基類,預設為filterset.FilterSet
    filterset_base = filterset.FilterSet
    
    # 指示在過濾器驗證失敗時是否引發異常,預設為True,表示引發異常。
    raise_exception = True

    @property
    # 用於確定渲染過濾器表單時要使用的模板
    def template(self):
        
        # 如果使用Crispy Forms
        if compat.is_crispy():
            
            # 則為"django_filters/rest_framework/crispy_form.html"
            return "django_filters/rest_framework/crispy_form.html"
        
        # 否則為"django_filters/rest_framework/form.html"。
        return "django_filters/rest_framework/form.html"
	
    # 返回過濾器集的例項
    def get_filterset(self, request, queryset, view):
        
        # 使用get_filterset_class方法來獲取用於過濾查詢集的FilterSet類。
        # 這個類可以在檢視中透過filterset_class屬性指定,如果未指定,則會自動建立一個基於查詢集的FilterSet類。
        filterset_class = self.get_filterset_class(view, queryset)
        
        # 檢查過濾器集類:如果獲取到了過濾器集類(filterset_class不為None),則繼續執行下面的步驟。
        # 否則,返回None,表示沒有可用的過濾器集。
        if filterset_class is None:
            return None
		
        # 獲取過濾器集的關鍵字引數
        # 呼叫get_filterset_kwargs方法,以獲取傳遞給過濾器集建構函式的關鍵字引數
        # 。這些引數通常包括請求資料、查詢集和請求物件等。
        kwargs = self.get_filterset_kwargs(request, queryset, view)
        
        # 建立過濾器集物件
        # 最後,它使用獲取到的過濾器集類和關鍵字引數來建立過濾器集物件,並將其返回。
        return filterset_class(**kwargs)
	
    # 返回用於過濾查詢集的FilterSet類
    def get_filterset_class(self, view, queryset=None):
        """
        # 這個方法會根據檢視和查詢集的情況來確定應該使用哪個 FilterSet 類
        Return the `FilterSet` class used to filter the queryset.
        """
        
        # 嘗試從檢視中獲取 filterset_class 屬性。這個屬性是檢視中定義的,可以指定要用於過濾的 FilterSet 類。
        # 如果檢視中有指定,則直接返回這個類。
        filterset_class = getattr(view, "filterset_class", None)
        
        # 如果檢視中沒有指定 filterset_class,它會檢查是否有 filterset_fields 屬性。
        # filterset_fields 屬性是一組用於過濾的欄位名稱,通常與查詢集的模型相關。
        filterset_fields = getattr(view, "filterset_fields", None)
		
        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 類,該類的 Meta 類中包含了模型和欄位資訊。
            MetaBase = getattr(self.filterset_base, "Meta", object)

            class AutoFilterSet(self.filterset_base):
                class Meta(MetaBase):
                    model = queryset.model
                    fields = filterset_fields

            return AutoFilterSet
        
		 # 如果既沒有指定 filterset_class 也沒有 filterset_fields,則返回 None,表示沒有可用的 FilterSet 類。
        return None
	
    # 此方法返回傳遞給過濾器集建構函式的關鍵字引數,包括請求資料、查詢集和請求物件。
    def get_filterset_kwargs(self, request, queryset, view):
        return {
            "data": request.query_params,
            "queryset": queryset,
            "request": request,
        }
	
    # 此方法用於過濾查詢集。,,。
    def filter_queryset(self, request, queryset, view):
        # 透過呼叫 get_filterset 方法,獲取與檢視關聯的過濾器集例項 filterset。
        filterset = self.get_filterset(request, queryset, view)
        
        # 檢查 filterset 是否為 None。
        if filterset is None:
            # 如果 filterset 為 None,說明沒有可用的過濾器集與檢視關聯,此時直接返回原始的查詢集 queryset,不進行任何過濾。
            return queryset
		
        # 如果 filterset 存在,方法會進一步驗證過濾器集的有效性,即呼叫 is_valid 方法檢查過濾器集是否透過了驗證。
        if not filterset.is_valid() and self.raise_exception:
             # 如果過濾器集無效(is_valid 返回 False)並且 self.raise_exception 屬性為 True,則會丟擲驗證異常,該異常包含了過濾器集的錯誤資訊。
            raise utils.translate_validation(filterset.errors)
            
        # 然後將其應用於查詢集
        return filterset.qs
	
    # 此方法返回過濾器表單的HTML表示。
    def to_html(self, request, queryset, view):
        # 呼叫 get_filterset 方法獲取過濾器集例項 filterset。
        filterset = self.get_filterset(request, queryset, view)
        
        # 如果 filterset 為 None,則返回 None,表示沒有可用的過濾器集,因此無法生成過濾器表單的 HTML 表示。
        if filterset is None:
            return None
		
        # 如果 filterset 存在,方法會根據指定的模板(self.template)渲染過濾器表單。
        # 通常,模板會包含 HTML 表單元素,以顯示過濾器欄位和相應的輸入框、核取方塊等表單元件。
        template = loader.get_template(self.template)
        context = {"filter": filterset}
        
        # 最終,方法返回渲染後的 HTML 表示,以便在前端頁面中顯示過濾器表單
        return template.render(context, request)
	
    # 此方法返回與過濾器欄位相關的CoreAPI欄位定義
    def get_coreschema_field(self, field):
        # 首先,它根據過濾器欄位的型別判斷,
        if isinstance(field, filters.NumberFilter):
            
            # 如果欄位型別是 filters.NumberFilter,則生成一個 compat.coreschema.Number 欄位
            field_cls = compat.coreschema.Number
        else:
            
            # 否則生成一個 compat.coreschema.String 欄位。
            field_cls = compat.coreschema.String
            
        # 欄位的 description 屬性通常設定為過濾器欄位的幫助文字(help_text),以提供關於欄位用途的描述資訊。
        # 最終,方法返回生成的 CoreAPI 欄位定義,用於 API 文件的生成和展示
        return field_cls(description=str(field.extra.get("help_text", "")))
	
    # 此方法返回API操作的引數定義列表,用於生成API文件。它檢查過濾器集的基本過濾器,併為每個欄位建立一個引數定義。
    def get_schema_fields(self, view):
        # This is not compatible with widgets where the query param differs from the
        # filter's attribute name. Notably, this includes `MultiWidget`, where query
        # params will be of the format `<name>_0`, `<name>_1`, etc...
        
        # 檢查 Django 的 RemovedInDjangoFilter25Warning
        from django_filters import RemovedInDjangoFilter25Warning
        
        # 併發出警告,指出內建的模式生成已被棄用,建議使用 drf-spectacular。
        warnings.warn(
            "Built-in schema generation is deprecated. Use drf-spectacular.",
            category=RemovedInDjangoFilter25Warning,
        )
        
        # 斷言 coreapi 和 coreschema 庫已安裝,因為這些庫用於生成 API 文件的引數欄位。
        assert (
            compat.coreapi is not None
        ), "coreapi must be installed to use `get_schema_fields()`"
        assert (
            compat.coreschema is not None
        ), "coreschema must be installed to use `get_schema_fields()`"
		
        # 方法嘗試從檢視中獲取查詢集(queryset)物件。
        try:
            # 這是透過呼叫檢視的 get_queryset() 方法來完成的,以便獲取與檢視關聯的查詢集。
            queryset = view.get_queryset()
        except Exception:
            
            # 如果無法獲取查詢集(通常是因為檢視沒有實現 get_queryset() 方法),則將 queryset 設定為 None
            queryset = None
            # 併發出警告,指出該檢視不相容模式生成。
            warnings.warn(
                "{} is not compatible with schema generation".format(view.__class__)
            )
		
        # 使用 get_filterset_class 方法來獲取與檢視關聯的過濾器集類(filterset_class)
        filterset_class = self.get_filterset_class(view, queryset)

        return (
            []
            # 如果過濾器集類已經在檢視中定義(透過 filterset_class 屬性),則使用該類
            if not filterset_class
            
            # 如果沒有可用的過濾器集類,方法返回一個空列表 [],表示沒有需要生成引數欄位的過濾器。
            else [
                # 否則,如果檢視定義了 filterset_fields 屬性且查詢集不為 None,則會動態建立一個自動生成的過濾器集類 AutoFilterSet,該類繼承自 filterset_base,並使用查詢集的模型和 filterset_fields 屬性來定義過濾器集。
                compat.coreapi.Field(
                    
                    # name 屬性設定為欄位的名稱。
                    name=field_name,
                    
                    # required 屬性根據欄位的 required 屬性設定。
                    required=field.extra["required"],
                    
                    # location 屬性設定為 "query",表示這些引數位於查詢字串中
                    location="query",
                    
                    # schema 屬性透過呼叫 get_coreschema_field 方法生成,該方法生成與欄位相關的 CoreAPI 欄位定義
                    schema=self.get_coreschema_field(field),
                )
                # 
                for field_name, field in filterset_class.base_filters.items()
            ]
        )
	
    # 此方法返回API操作的引數列表,用於生成API文件。
    # 它與get_schema_fields類似,但提供了更詳細的引數資訊,包括型別、是否必需等。
    def get_schema_operation_parameters(self, view):
        
        # 檢查 Django 的 RemovedInDjangoFilter25Warning
        from django_filters import RemovedInDjangoFilter25Warning
        
        # 併發出警告,指出內建的模式生成已被棄用,建議使用 drf-spectacular
        warnings.warn(
            "Built-in schema generation is deprecated. Use drf-spectacular.",
            category=RemovedInDjangoFilter25Warning,
        )
        try:
            # 嘗試從檢視中獲取查詢集(queryset)物件,以便確定與檢視關聯的模型。
            queryset = view.get_queryset()
        except Exception:
            
            # 如果無法獲取查詢集(通常是因為檢視沒有實現 get_queryset() 方法),則將 queryset 設定為 None
            queryset = None
            
            # 併發出警告,指出該檢視不相容模式生成。
            warnings.warn(
                "{} is not compatible with schema generation".format(view.__class__)
            )
		
        # 使用 get_filterset_class 方法來獲取與檢視關聯的過濾器集類(filterset_class)
        # 如果過濾器集類已經在檢視中定義(透過 filterset_class 屬性),則使用該類
        # 否則,如果檢視定義了 filterset_fields 屬性且查詢集不為 None,則會動態建立一個自動生成的過濾器集類 AutoFilterSet,該類繼承自 filterset_base,並使用查詢集的模型和 filterset_fields 屬性來定義過濾器集。
        filterset_class = self.get_filterset_class(view, queryset)
        
        # 如果沒有可用的過濾器集類,方法返回一個空列表 [],表示沒有需要生成引數欄位的過濾器。
        if not filterset_class:
            return []
		
        parameters = []
        # # 遍歷過濾器集類的基本過濾器欄位(base_filters)併為每個欄位建立一個引數定義
        for field_name, field in filterset_class.base_filters.items():
            
            # 
            parameter = {
                
                # name 屬性設定為欄位的名稱。
                "name": field_name,
                
                # required 屬性根據欄位的 required 屬性設定。
                "required": field.extra["required"],
                
                # in 屬性設定為 "query",表示這些引數位於查詢字串中。
                "in": "query",
                
                # description 屬性設定為欄位的標籤(label),如果欄位沒有標籤,則設定為欄位的名稱。
                "description": field.label if field.label is not None else field_name,
                
                # schema 屬性是一個包含引數型別資訊的字典。在這裡,所有引數都被表示為字串("type": "string")。
                "schema": {
                    "type": "string",
                },
            }
            
            # 如果欄位的 extra 屬性包含了 "choices" 鍵(即欄位具有選項)
            if field.extra and "choices" in field.extra:
                
                # 將 schema 字典的 "enum" 鍵設定為欄位選項的列表。
                parameter["schema"]["enum"] = [c[0] for c in field.extra["choices"]]
                
            parameters.append(parameter)
        return parameters

【四】自定義過濾類

【1】使用

from rest_framework import filters
from django.db.models import Q


class BookFilter(filters.BaseFilterBackend):
    '''
        def filter_queryset(self, request, queryset, view):
        """
        Return a filtered queryset.
        """
        raise NotImplementedError(".filter_queryset() must be overridden.")
    '''

    def filter_queryset(self, request, queryset, view):
        # 返回的資料,都是過濾後的資料
        # http://127.0.0.1:8000/app01/v1/books/?price=99&name=追夢赤子心
        # 需要將篩選條件變成price=99或name=追夢赤子心,而第三方寫法中訪問地址只能是上面的格式
        price = request.query_params.get('price')
        name = request.query_params.get('name')
        queryset = queryset.filter(Q(name=name) | Q(price=price))

        return queryset

【2】分析

  • 根據位址列中傳入的資料進行過濾
  • 將過濾完的 queryset 資料集返回

相關文章