带有 DRF 的 Django-filter - 如何在使用相同查找应用多个值时执行“和”?

Posted

技术标签:

【中文标题】带有 DRF 的 Django-filter - 如何在使用相同查找应用多个值时执行“和”?【英文标题】:Django-filter with DRF - How to do 'and' when applying multiple values with the same lookup? 【发布时间】:2017-05-02 19:53:48 【问题描述】:

这是我正在使用的过滤器集的一个稍微简化的示例,我将它与用于 Django Rest 框架的 DjangoFilterBackend 一起使用。我希望能够向/api/bookmarks/?title__contains=word1&title__contains=word2 发送请求并返回包含两个单词的结果,但目前它忽略第一个参数并且仅过滤word2。

任何帮助将不胜感激!

class BookmarkFilter(django_filters.FilterSet):

    class Meta:
        model = Bookmark
        fields = 
            'title': ['startswith', 'endswith', 'contains', 'exact', 'istartswith', 'iendswith', 'icontains', 'iexact'],
        

class BookmarkViewSet(viewsets.ModelViewSet):
    serializer_class = BookmarkSerializer
    permission_classes = (IsAuthenticated,)
    filter_backends = (DjangoFilterBackend,)
    filter_class = BookmarkFilter
    ordering_fields = ('title', 'date', 'modified')
    ordering = '-modified'
    page_size = 10

【问题讨论】:

您是否也可以粘贴使用定义的过滤器后端的视图代码。 在这里查看问题filter on a field for a set of values (OR between values) @shady 我已经阅读了那个问题线程。您链接到的代码为值之间的 ORing 但不是 AND 提供了解决方案。上面发布的视图代码。感谢您的帮助。 好吧,那么您应该考虑改写 get_queryset。@ergusto 你能解释一下这会有什么帮助吗?初始查询集不会影响过滤的执行方式。 @shady 【参考方案1】:

主要问题是您需要一个了解如何对多个值进行操作的过滤器。基本上有两种选择:

使用MultipleChoiceFilter(不推荐用于此实例) 编写自定义过滤器类

使用MultipleChoiceFilter

class BookmarkFilter(django_filters.FilterSet):
    title__contains = django_filters.MultipleChoiceFilter(
        name='title',
        lookup_expr='contains',
        conjoined=True,  # uses AND instead of OR
        choices=[???],
    )

    class Meta:
        ...

虽然这保留了您想要的语法,但问题是您必须构建一个选项列表。我不确定您是否可以简化/减少可能的选择,但是您似乎需要从数据库中获取所有标题,将标题拆分为不同的单词,然后创建一个集合以删除重复项。根据您拥有的记录数量,这似乎会很昂贵/很慢。

自定义Filter

或者,您可以创建一个自定义过滤器类 - 如下所示:

class MultiValueCharFilter(filters.BaseCSVFilter, filters.CharFilter):
    def filter(self, qs, value):
        # value is either a list or an 'empty' value
        values = value or []

        for value in values:
            qs = super(MultiValueCharFilter, self).filter(qs, value)

        return qs


class BookmarkFilter(django_filters.FilterSet):
    title__contains = MultiValueCharFilter(name='title', lookup_expr='contains')

    class Meta:
        ...

用法(注意值以逗号分隔):

GET /api/bookmarks/?title__contains=word1,word2

结果:

qs.filter(title__contains='word1').filter(title__contains='word2')

语法有所改变,但基于 CSV 的过滤器不需要构造不必要的选择集。

请注意,实际上不可能支持 ?title__contains=word1&title__contains=word2 语法,因为小部件无法呈现合适的 html 输入。您要么需要使用 SelectMultiple(这同样需要选择),要么在客户端上使用 javascript 添加/删除具有相同 name 属性的其他文本输入。


不用过多介绍,过滤器和过滤器集只是 Django 表单的扩展。

Filter 的形式为 Field,而 Widget 的形式又为 WidgetFilterSetFilters 组成。 FilterSet 根据其过滤器的字段生成内部表单。

每个过滤器组件的职责:

小部件从data QueryDict 检索原始值。 该字段验证原始值。 过滤器使用经过验证的值构造对查询集的filter() 调用。

为了对同一个过滤器应用多个值,您需要一个过滤器、字段和小部件来了解如何对多个值进行操作。


自定义过滤器通过混合BaseCSVFilter 来实现这一点,而BaseCSVFilter 又将“逗号分隔=> 列表”功能混合到组合字段和小部件类中。

我建议查看 CSV mixin 的源代码,但简而言之:

widget 将传入的值拆分为值列表。 field 通过验证“主”字段类(例如CharFieldIntegerField)上的各个值来验证整个值列表。该字段还派生了混合的小部件。 filter 只是派生混合字段类。

CSV 过滤器旨在与接受值列表的inrange 查找一起使用。在这种情况下,contains 需要一个值。 filter() 方法通过迭代值并将各个过滤器调用链接在一起来解决此问题。

【讨论】:

首先非常感谢您提供如此详尽的帖子。它澄清了很多我模糊理解的东西。我尝试实现您的 MultiValueCharFilter 并且不幸地使用逗号语法过滤多个值返回 0 结果,但没有错误。不过,用一个值过滤效果很好。我查看了源代码,据我所知它应该可以工作,所以不确定它有什么问题。我认为这将与大多数其他类型的过滤器代替 CharFilter 一起使用是否正确?再次,非常感谢。 是的,我认为这应该是大致正确的。我实际上并没有花时间测试它。您是正确的,它应该与其他类型的过滤器一起使用,例如NumberFilter。也就是说,只有少数几个地方真正有意义。 很遗憾,使用 BaseCSVFilter 的解决方案在 title 包含逗号时无法正常工作 MultiValueFilter 来自哪里? MultiValueFilter 是一个错字。应该是MultiValueCharFilter【参考方案2】:

您可以像这样创建自定义列表字段:

from django.forms.widgets import SelectMultiple
from django import forms

class ListField(forms.Field):
    widget = SelectMultiple

    def __init__(self, field, *args, **kwargs):
        super(ListField, self).__init__( *args, **kwargs)
        self.field = field

    def validate(self, value):
        super(ListField, self).validate(value)
        for val in value:
            self.field.validate(val)

    def run_validators(self, value):
        for val in value:
            self.field.run_validators(val)

    def to_python(self, value):
        if not value:
            return []
        elif not isinstance(value, (list, tuple)):
            raise ValidationError(self.error_messages['invalid_list'], code='invalid_list')
        return [self.field.to_python(val) for val in value]

并使用 MultipleChoiceFilter 创建自定义过滤器:

class ContainsListFilter(django_filters.MultipleChoiceFilter):
    field_class = ListField

    def get_filter_predicate(self, v):
        name = '%s__contains' % self.name
        try:
            return name: getattr(v, self.field.to_field_name)
        except (AttributeError, TypeError):
            return name: v

之后,您可以使用自定义过滤器创建 FilterSet:

from django.forms import CharField

class StorageLocationFilter(django_filters.FilterSet):
    title_contains = ContainsListFilter(field=CharField())

为我工作。希望对你有用。

【讨论】:

【参考方案3】:

这是一个可以正常工作的示例代码: 它支持 - product?name=p1,p2,p3 并将返回名称为 (p1,p2,p3) 的产品

def resolve_csvfilter(queryset, name, value):
    lookup =  f'name__in': value.split(",") 
    queryset = queryset.filter(**lookup)
    return queryset

class ProductFilterSet(FilterSet):
        name = CharFilter(method=resolve_csvfilter)
    
        class Meta:
            model = Product
            fields = ['name']

参考:https://django-filter.readthedocs.io/en/master/guide/usage.html#customize-filtering-with-filter-method https://github.com/carltongibson/django-filter/issues/137

【讨论】:

以上是关于带有 DRF 的 Django-filter - 如何在使用相同查找应用多个值时执行“和”?的主要内容,如果未能解决你的问题,请参考以下文章

Django(69)最好用的过滤器插件Django-filter

drf 过滤器组件与自定义过滤器

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

DRF的过滤与排序

在大表上使用 Django-Filter 以及 DataTables2

带有 Knox 代币的 DRF 的 Djoser