Django-filter:按模型属性过滤

Posted

技术标签:

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

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

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

我的问题是:是否有任何库允许通过内存中的属性过滤查询集?如果不是,那么查询集究竟必须如何被篡改才能使这成为可能?以及如何将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']

【讨论】:

以上是关于Django-filter:按模型属性过滤的主要内容,如果未能解决你的问题,请参考以下文章

你如何使用带有参数列表的 django-filter 包?

如何使用 Django-filter 过滤多个字段?

django-filter使用分页

python测试开发django-169.过滤器django-filter 入门使用

Python测试开发django-169.过滤器django-filter 入门使用

如何在 ListAPIView 中使用 django-filter 对过滤结果进行排序