带有 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
的形式又为 Widget
。
FilterSet
由Filter
s 组成。
FilterSet
根据其过滤器的字段生成内部表单。
每个过滤器组件的职责:
小部件从data
QueryDict
检索原始值。
该字段验证原始值。
过滤器使用经过验证的值构造对查询集的filter()
调用。
为了对同一个过滤器应用多个值,您需要一个过滤器、字段和小部件来了解如何对多个值进行操作。
自定义过滤器通过混合BaseCSVFilter
来实现这一点,而BaseCSVFilter
又将“逗号分隔=> 列表”功能混合到组合字段和小部件类中。
我建议查看 CSV mixin 的源代码,但简而言之:
widget 将传入的值拆分为值列表。 field 通过验证“主”字段类(例如CharField
或IntegerField
)上的各个值来验证整个值列表。该字段还派生了混合的小部件。
filter 只是派生混合字段类。
CSV 过滤器旨在与接受值列表的in
和range
查找一起使用。在这种情况下,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