如何在 Django 中动态地向查询添加过滤器?

How to add filters to a query dynamically in Django?

在我的 viewSet 中我正在做一个查询,

queryset= Books.objects.all();

现在,通过 ajax 调用,我从 UI 中获取过滤器值,即 auther.There 的年龄、性别等总共有 5 个过滤器。

现在我 运行 遇到的问题是如何将过滤器添加到我的查询中(仅那些具有任何值的过滤器)。

我尝试的是检查单个过滤器值并进行查询,但那样就失败了,就好像用户删除了过滤器值或添加了多个过滤器。 有更好的建议如何实现吗?

您没有显示任何代码,所以您没有真正解释问题所在:

从查询集开始 Book.objects.all()。对于每个过滤器,检查 request.POST 中是否有过滤器的值,如果有,则过滤查询集。 Django 查询集是惰性的,因此只会评估最终的查询集。

queryset = Book.objects.all()
if request.POST.get('age'):
    queryset = queryset.filter(author__age=request.POST['age'])
if request.POST.get('gender'):
    queryset = queryset.filter(author__gender=request.POST['gender'])
...

这里有一个更通用的。如果它们作为 GET 参数传递,它将对您的查询集应用过滤器。如果您正在进行 POST 调用,只需更改代码中的名称即可。

import operator
from django.db.models import Q


def your_view(self, request, *args, **kwargs):
    # Here you list all your filter names
    filter_names = ('filter_one', 'filter_two', 'another_one', )

    queryset = Books.objects.all(); 
    filter_clauses = [Q(filter=request.GET[filter])
                      for filter in filter_names
                      if request.GET.get(filter)]
    if filter_clauses:
        queryset = queryset.filter(reduce(operator.and_, filter_clauses))

    # rest of your view

请注意,您可以在过滤器名称中使用查找表达式。例如,如果你想过滤价格低于或等于过滤器中指定的书籍,你可以只使用 price__lte 作为过滤器名称。

也许 django-filter 可以帮助简化其他人给出的解决方案?

类似于:

class BookFilter(django_filters.FilterSet):
    class Meta:
        model = Book
        fields = ['author__age', 'author__gender', ...]

然后视图看起来像:

def book_list(request):
    f = BookFilter(request.GET, queryset=Book.objects.all())
    return render_to_response('my_app/template.html', {'filter': f})

有关详细信息,请参阅 documentation

您可以简单地将 request.GET 内容作为字典获取(确保将值转换为字符串或所需的类型,因为它们默认会列出,即:dict(request.GET) 会给您像 {u'a': [u'val']}.

一旦你确定你有一个键字典匹配你的模型字段,你可以简单地做:

filtered = queryset.filter(**dict_container)

这对我有用,我已将 Alex Morozov 的回答与

合并
import operator

def your_view(self, request, *args, **kwargs):
    # Here you list all your filter names
    filter_names = ('filter_one', 'filter_two', 'another_one', )

    queryset = Books.objects.all(); 
    filter_clauses = [Q(**{filter: request.GET[filter]})
                  for filter in filter_names
                  if request.GET.get(filter)]
    if filter_clauses:
    queryset = queryset.filter(reduce(operator.and_, filter_clauses))

    # rest of your view

你可以这样做

class BooksAPI(viewsets.ModelViewSet):
    queryset = Books.objects.none()


def get_queryset(self):
    argumentos = {}
    if self.request.query_params.get('age'):
        argumentos['age'] = self.request.query_params.get('age')
    if self.request.query_params.get('gender'):
        argumentos['gender'] = self.request.query_params.get('gender')
    if len(argumentos) > 0:
        books = Books.objects.filter(**argumentos)
    else:
        books = Books.objects.all()
    return books

对于非常简单的相等性检查,这是我在 ModelViewSet 中使用辅助函数的解决方案。

  1. 辅助函数 check_for_params 创建一个在 URL.

    中传递的请求参数的字典
  2. 更改 ModelViewSet get_queryset() 方法,通过使用单个过滤器子句过滤 Django QuerySet 来防止 multiple queries being called by chaining filters.

我可以尝试使用 Django Q() 对象,但无法让它只进行一次调用。

def check_for_params(request, param_check_list: List) -> dict:
    """
    Create a dictionary of params passed through URL.

    Parameters
    ----------
    request - DRF Request object.
    param_check_list - List of params potentially passed in the url.
    """
    if not param_check_list:
        print("No param_check_list passed.")
    else:
        param_dict = {}
        for p in param_check_list:
            param_dict[p] = request.query_params.get(p, None)
        return param_dict
    

class MyModelViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer
    authentication_classes = [SessionAuthentication]
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        """
        Return a queryset and apply filters, if applicable.
        
        Info
        ----
        Building the queryset.filter method by unpacking the key-value pairs this way,
        creates a single filter clause and prevents multiple queries from being called
        by chaining filters.
        """
        queryset = MyModel.objects.all()

        param_check_list = ['param1', 'param2', 'param3']
        params = check_for_params(self.request, param_check_list)
        filtered = {k: v for k, v in params.items() if v}

        # Calling filter() only once here prevents multiple queries. 
        return queryset.filter(**filtered)