Django-Filter原始碼解析一
阿新 • • 發佈:2018-12-13
Django Filter原始碼解析
最近在看Django-FIlter專案的原始碼,學習一下別人的開發思想;
整體介紹
首先,我從其中一個測試用例作為入口,開始了debug之路,一點一點的斷點,分析它的執行順序,如圖:
ok,下面從程式碼的層面進行分析:
url
url(r'^books/$', FilterView.as_view(model=Book)),
view函式,這裡的實現方式應該是借鑑了Django中自帶的ListView,其同樣的繼承了MultipleObjectTemplateResponseMixin, BaseListView,繼承的好處在於可以複用其已經封裝好的方法,最終可以簡單的實現展示,
class FilterView(MultipleObjectTemplateResponseMixin, BaseFilterView): """ Render some list of objects with filter, set by `self.model` or `self.queryset`. `self.queryset` can actually be any iterable of items, not just a queryset. """ template_name_suffix = '_filter'
基礎過濾view,這裡做的就是類似BaseListView的功能,獲取計算出來的查詢集,將結果渲染後返回;
class BaseFilterView(FilterMixin, MultipleObjectMixin, View): """ 顯示物件的過濾功能的基view,實現的方式類似BaseListView """ def get(self, request, *args, **kwargs): # 獲取過濾的類 filterset_class = self.get_filterset_class() # 傳入類,構造引數,返回類的物件 self.filterset = self.get_filterset(filterset_class
接下來就分成三件事:a.獲取過濾類,b.根據過濾類獲取過濾物件,c.過濾,下面的程式碼就做到了前面兩步;
class FilterMixin(metaclass=FilterMixinRenames): """ A mixin that provides a way to show and handle a FilterSet in a request. 提供控制過濾的方法 """ def get_filterset_class(self): """ Returns the filterset class to use in this view 返回過濾類 """ if self.filterset_class: # 避免重複建立 return self.filterset_class elif self.model: # 使用了工廠模式 return filterset_factory(model=self.model, fields=self.filterset_fields) else: msg = "'%s' must define 'filtserset_class' or 'model'" raise ImproperlyConfigured(msg % self.__class__.__name__) def get_filterset(self, filterset_class): """ Returns an instance of the filterset to be used in this view. """ kwargs = self.get_filterset_kwargs(filterset_class) return filterset_class(**kwargs)def filterset_factory(model, fields=ALL_FIELDS): # 根據model生成相對應的FilterSet,比如model是Book,那麼就會生成BookFilterSet的例項 meta = type(str('Meta'), (object,), {'model': model, 'fields': fields}) # 使用type進行建立類,並且繼承了FilterSet類 filterset = type(str('%sFilterSet' % model._meta.object_name), (FilterSet,), {'Meta': meta}) return filterset
接下來就是重頭戲,開始過濾了!下面會被呼叫是因為呼叫了FilterSet中的qs方法;
class BaseFilterSet(object): # ... def __init__(self, data=None, queryset=None, *, request=None, prefix=None): # 如果沒傳進來則在全部的基礎進行過濾 if queryset is None: queryset = self._meta.model._default_manager.all() model = queryset.model # ... self.filters = copy.deepcopy(self.base_filters) # propagate the model and filterset to the filters for filter_ in self.filters.values(): filter_.model = model filter_.parent = self 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會在每次執行後的queryset上繼續執行,達到過濾的效果 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 @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
或許你會疑惑self.filters
(self.base_filters
)裡面的內容是什麼,其實就是每個需要過濾的資料庫欄位到具體的Filter的對映,那這個是哪裡進行計算賦值的呢?其實是被元類給攔截了,下面則會把該的內容是從類的get_filters方法中獲取得到的,
class FilterSet(BaseFilterSet, metaclass=FilterSetMetaclass): passclass FilterSetMetaclass(type): # 元類,FilterSet建立時最終會建立FilterSetMetaclass的例項 def __new__(cls, name, bases, attrs): ... new_class = super().__new__(cls, name, bases, attrs) new_class._meta = FilterSetOptions(getattr(new_class, 'Meta', None)) new_class.base_filters = new_class.get_filters() # 會被class BaseFilterSet(object): # ... @classmethod def get_filters(cls): """ Get all filters for the filterset. This is the combination of declared and generated filters. 獲取到每個需要過濾的資料庫欄位到Filter的對映 比如:{title: CharFilter} """ # No model specified - skip filter generation if not cls._meta.model: return cls.declared_filters.copy() # Determine the filters that should be included on the filterset. filters = OrderedDict() fields = cls.get_fields() undefined = [] for field_name, lookups in fields.items(): field = get_model_field(cls._meta.model, field_name) # warn if the field doesn't exist. if field is None: undefined.append(field_name) for lookup_expr in lookups: filter_name = cls.get_filter_name(field_name, lookup_expr) # If the filter is explicitly declared on the class, skip generation if filter_name in cls.declared_filters: filters[filter_name] = cls.declared_filters[filter_name] continue if field is not None: filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr) # filter out declared filters undefined = [f for f in undefined if f not in cls.declared_filters] if undefined: raise TypeError( "'Meta.fields' contains fields that are not defined on this FilterSet: " "%s" % ', '.join(undefined) ) # Add in declared filters. This is necessary since we don't enforce adding # declared filters to the 'Meta.fields' option filters.update(cls.declared_filters) return filters @classmethod def filter_for_field(cls, field, field_name, lookup_expr='exact'): field, lookup_type = resolve_field(field, lookup_expr) default = { 'field_name': field_name, 'lookup_expr': lookup_expr, } filter_class, params = cls.filter_for_lookup(field, lookup_type) default.update(params) assert filter_class is not None, ( "%s resolved field '%s' with '%s' lookup to an unrecognized field " "type %s. Try adding an override to 'Meta.filter_overrides'. See: " "https://django-filter.readthedocs.io/en/master/ref/filterset.html" "#customise-filter-generation-with-filter-overrides" ) % (cls.__name__, field_name, lookup_expr, field.__class__.__name__) return filter_class(**default) @classmethod def filter_for_lookup(cls, field, lookup_type): """ 過濾 :param field: :param lookup_type: :return: """ DEFAULTS = dict(cls.FILTER_DEFAULTS) if hasattr(cls, '_meta'): DEFAULTS.update(cls._meta.filter_overrides) data = try_dbfield(DEFAULTS.get, field.__class__) or {} filter_class = data.get('filter_class') params = data.get('extra', lambda field: {})(field) # if there is no filter class, exit early if not filter_class: return None, {} # perform lookup specific checks if lookup_type == 'exact' and getattr(field, 'choices', None): return ChoiceFilter, {'choices': field.choices} if lookup_type == 'isnull': data = try_dbfield(DEFAULTS.get, models.BooleanField) filter_class = data.get('filter_class') params = data.get('extra', lambda field: {})(field) return filter_class, params if lookup_type == 'in': class ConcreteInFilter(BaseInFilter, filter_class): pass ConcreteInFilter.__name__ = cls._csv_filter_class_name( filter_class, lookup_type ) return ConcreteInFilter, params if lookup_type == 'range': class ConcreteRangeFilter(BaseRangeFilter, filter_class): pass ConcreteRangeFilter.__name__ = cls._csv_filter_class_name( filter_class, lookup_type ) return ConcreteRangeFilter, params return filter_class, params
具體的資料庫欄位型別對應的Filter如下,上面也就是根據這些來找到對應的Filter,發現沒,是BaseFilterSet類的FILTER_DEFAULTS變數
FILTER_FOR_DBFIELD_DEFAULTS = { models.AutoField: {'filter_class': NumberFilter}, models.CharField: {'filter_class': CharFilter}, models.TextField: {'filter_class': CharFilter}, models.BooleanField: {'filter_class': BooleanFilter}, models.DateField: {'filter_class': DateFilter}, models.DateTimeField: {'filter_class': DateTimeFilter}, models.TimeField: {'filter_class': TimeFilter}, models.DurationField: {'filter_class': DurationFilter}, models.DecimalField: {'filter_class': NumberFilter}, models.SmallIntegerField: {'filter_class': NumberFilter}, models.IntegerField: {'filter_class': NumberFilter}, models.PositiveIntegerField: {'filter_class': NumberFilter}, models.PositiveSmallIntegerField: {'filter_class': NumberFilter}, models.FloatField: {'filter_class': NumberFilter}, models.NullBooleanField: {'filter_class': BooleanFilter}, models.SlugField: {'filter_class': CharFilter}, models.EmailField: {'filter_class': CharFilter}, models.FilePathField: {'filter_class': CharFilter}, models.URLField: {'filter_class': CharFilter}, models.GenericIPAddressField: {'filter_class': CharFilter}, models.CommaSeparatedIntegerField: {'filter_class': CharFilter}, models.UUIDField: {'filter_class': UUIDFilter}, # Forward relationships models.OneToOneField: { 'filter_class': ModelChoiceFilter, 'extra': lambda f: { 'queryset': remote_queryset(f), 'to_field_name': f.remote_field.field_name, 'null_label': settings.NULL_CHOICE_LABEL if f.null else None, } }, models.ForeignKey: { 'filter_class': ModelChoiceFilter, 'extra': lambda f: { 'queryset': remote_queryset(f), 'to_field_name': f.remote_field.field_name, 'null_label': settings.NULL_CHOICE_LABEL if f.null else None, } }, models.ManyToManyField: { 'filter_class': ModelMultipleChoiceFilter, 'extra': lambda f: { 'queryset': remote_queryset(f), } }, # Reverse relationships OneToOneRel: { 'filter_class': ModelChoiceFilter, 'extra': lambda f: { 'queryset': remote_queryset(f), 'null_label': settings.NULL_CHOICE_LABEL if f.null else None, } }, ManyToOneRel: { 'filter_class': ModelMultipleChoiceFilter, 'extra': lambda f: { 'queryset': remote_queryset(f), } }, ManyToManyRel: { 'filter_class': ModelMultipleChoiceFilter, 'extra': lambda f: { 'queryset': remote_queryset(f), } },}
ok,到這裡就簡單的介紹完畢了。