【问题标题】:Django-filter: filtering by model propertyDjango-filter:按模型属性过滤
【发布时间】:2019-08-23 19:01:07
【问题描述】:

我在severalplaces 上读到,无法使用属性过滤 Django 查询集,因为 Django ORM 不知道如何将它们转换为 SQL。

但是,一旦数据被提取并加载到内存中,应该可以使用这些属性在 Python 中过滤它们。

我的问题是:是否有任何库允许通过内存中的属性过滤查询集?如果不是,那么查询集究竟必须如何被篡改才能使这成为可能?以及如何将django-filter 包含在其中?

【问题讨论】:

    标签: django properties django-queryset django-filter


    【解决方案1】:

    你有困难的财产吗? 如果没有,您可以像这样将其重写为查询集:

    from django.db import models
    
    class UserQueryset(models.Manager):
    
        def get_queryset(self):
    
            return super().get_queryset().annotate(
                has_profile=models.Exists(Profile.objects.filter(user_id=models.OuterRef('id')))
            )
    
    class User(models.Model):
        objects = UserQueryset
    
    
    class Profile(models.Model):
        user = models.OneToOneField(User, related_name='profile')
    
    
    # When you want to filter by has profile just use it like has field has profile
    
    user_with_profiles = User.objects.filter(has_profile=True)
    

    可能不是你想要的,但在某些情况下它可以帮助你

    【讨论】:

    • 我想要一个通用的 API 来处理各种属性,所以是的,我可能会遇到困难的。
    • @karloss,我建议通用 API 是 Django ORM 本身。几乎每次您通过 Django ORM 将过滤器推送到数据库时,它都会比加载该数据然后自己在 python 中过滤它要快。不是因为 python 太慢,而是因为您从数据库中提取了更多数据,而不是在数据库中对其进行过滤。为奇怪的推断属性编写自定义 django_filters.Filter 还不错。
    • @karloss,您还可以即时生成注释和过滤器。创建新过滤器,然后使用 Metaclasses 创建过滤器集。为了让客户端减少从服务器后端返回到自身的字段数量,我已经使用 Django Rest Framework 完成了这项工作。
    【解决方案2】:

    django-filter 想要并假设您正在使用查询集。一旦你获取了一个查询集并将其更改为 list,那么下游的任何东西都需要能够处理 list 或者只是遍历不再是查询集的列表。

    如果你有django_filters.FilterSet 喜欢:

    class FooFilterset(django_filters.FilterSet):
        bar = django_filters.Filter('updated', lookup_expr='exact')
        my_property_filter = MyPropertyFilter('property')
        class Meta:
            model = Foo
            fields = ('bar',  'my_property_filter')
    

    那么你可以写MyPropertyFilterlike:

    class MyPropertyFilter(django_filters.Filter):
        def filter(self, qs, value):
            return [row for row in qs if row.baz == value]
    

    此时,MyProperteyFilter 的任何下游都会有一个列表。

    注意:我相信fields 的顺序应该有你的自定义过滤器,MyPropertyFilter 最后,因为这样它总是会在普通查询集过滤器之后处理。


    所以,您刚刚破坏了“queryset”API,对于某些损坏的值。此时,您将不得不解决下游的任何错误。如果FilterSet 之后的任何内容需要.count 成员,您可以更改MyPropertyFilter 如下:

    class MyPropertyFilter(django_filters.Filter):
        def filter(self, qs, value):
            result = [row for row in qs if row.baz == value]
            result.count = len(result)
            return result
    

    你处于未知领域,你必须破解自己的方式。

    不管怎样,我以前做过,并不可怕。及时处理错误。

    【讨论】:

    • 返回一个QuerySet而不是一个列表不是更好吗?
    • 当然最好返回一个查询集。但你说你不能。如果您不能对数据库进行过滤,那么您必须在之后进行过滤。查询集是绝对可以在数据库上执行的东西。如果您可以更改您的属性以某种方式在数据库上执行,那么您可以完全避免这种情况。但是,如果你必须在实际的 python 中进行过滤,那么你就违反了查询集的本质,它不再是查询集。
    • 对不起,可能我表达得不好。我可以做一些像return QuerySet([row for row in qs if row.baz ==value]) 这样的事情,这样它就拥有.count.filter 等所有方法吗?当然,它们都只会在内存中工作,根本不会访问数据库,或者至少这是我的想法。我可以让这个以某种方式工作吗?
    • 没有将list 改回QuerySet 的默认方法。 .count 在列表情况下很容易,但是 .filter() 呢? Django ORM 不知道如何对list 进行过滤,它只对数据库进行过滤。无论如何,QuerySet API 的每个部分,您的下游代码正在使用,都需要实现。如果您正确订购过滤器,您只需将QuerySet -> list 过滤器放在fields 顺序的末尾即可。
    【解决方案3】:

    由于像property这样的非字段属性过滤不可避免地会将QuerySet转换为list(或类似的),所以我喜欢推迟它并在get_context_data方法中对object_list进行过滤。为了将过滤逻辑保留在filterset 类中,我使用了一个简单的技巧。我已经定义了一个decorator

    def attr_filter(func):
    
        def wrapper(self, queryset, name, value, force=False, *args, **kwargs):
            if force:
                return func(self, queryset, name, value, *args, **kwargs)
            else:
                return queryset
        return wrapper
    

    用于django-filter 非字段过滤方法。感谢这个装饰器,过滤基本上什么都不做(或跳过)非字段过滤方法(因为force=False默认值)。

    接下来,我定义了一个Mixin 用于view 类。

        class FilterByAttrsMixin:
        
            def get_context_data(self, **kwargs):
                context = super().get_context_data(**kwargs)
                filtered_list = self.filter_qs_by_attributes(self.object_list, self.filterset)
                context.update({
                    'object_list': filtered_list,
                })
                return context
        
            def filter_qs_by_attributes(self, queryset, filterset_instance):
                if hasattr(filterset_instance.form, 'cleaned_data'):
                    for field_name in filter_instance.filters:
                        method_name = f'attr_filter_{field_name}'
                        if hasattr(filterset_instance, method_name):
                            value = filterset_instance.form.cleaned_data[field_name]
                            if value:
                                queryset = getattr(filterset_instance, filter_method_name)(queryset, field_name, value, force=True)
                return queryset
    

    它基本上只是返回到您的filterset 并运行所有称为attr_filter_<field_name> 的方法,这次是force=True

    总之,您需要:

    • view 类中继承FilterByAttrsMixin
    • 调用你的过滤方法attr_filter_<field_name>
    • 在过滤方法上使用attr_filter装饰器

    简单示例(假设我有 model 称为 MyModelproperty 称为 is_static 我想过滤:

    型号:

    class MyModel(models.Model):
        ...
    
    @property
    def is_static(self):
        ...
    

    查看:

    class MyFilterView(FilterByAttrsMixin, django_filters.views.FilterView):
        ...
        filterset_class = MyFiltersetClass
        ...
    
    

    过滤器:

    class MyFiltersetClass(django_filters.FilterSet):
        is_static = django_filters.BooleanFilter(
            method='attr_filter_is_static',
        )
    
        class Meta:
            model = MyModel
            fields = [...]
    
        @attr_filter
        def attr_filter_is_static(self, queryset, name, value):
            return [instance for instance in queryset if instance.is_static]
    

    【讨论】:

      【解决方案4】:

      看看django-property-filter 包。这是django-filter 的扩展,提供按类属性过滤查询集的功能。

      文档中的简短示例:

      from django_property_filter import PropertyNumberFilter, PropertyFilterSet
      
      class BookFilterSet(PropertyFilterSet):
          prop_number = PropertyNumberFilter(field_name='discounted_price', lookup_expr='gte')
      
          class Meta:
              model = NumberClass
              fields = ['prop_number']
      

      【讨论】:

        猜你喜欢
        • 2020-01-19
        • 1970-01-01
        • 2020-12-21
        • 2017-05-05
        • 1970-01-01
        • 2011-09-20
        • 2017-12-12
        • 2016-01-24
        • 1970-01-01
        相关资源
        最近更新 更多