Django Tastypie高级过滤:如何使用Q对象进行复杂查询

28

我有一个基本的Django模型,如下所示:

class Business(models.Model):
    name = models.CharField(max_length=200, unique=True)
    email = models.EmailField()
    phone = models.CharField(max_length=40, blank=True, null=True)
    description = models.TextField(max_length=500)

我需要在上述模型上执行一个复杂的查询,例如:

qset = (
    Q(name__icontains=query) |
    Q(description__icontains=query) |
    Q(email__icontains=query)
    )
results = Business.objects.filter(qset).distinct()

我已经尝试使用tastypie,但没有成功:

def build_filters(self, filters=None):
    if filters is None:
        filters = {}
    orm_filters = super(BusinessResource, self).build_filters(filters)

    if('query' in filters):
        query = filters['query']
        print query
        qset = (
                Q(name__icontains=query) |
                Q(description__icontains=query) |
                Q(email__icontains=query)
                )
        results = Business.objects.filter(qset).distinct()
        orm_filters = {'query__icontains': results}

    return orm_filters

在 tastypie 的 Meta 类中,我将过滤设置为:

filtering = {
        'name: ALL,
        'description': ALL,
        'email': ALL,
        'query': ['icontains',],
    }

有什么想法可以解决这个问题吗?

谢谢 - 牛顿

3个回答

42

你已经走在了正确的道路上。然而,build_filters 应该将资源查找过渡到 ORM 查找。

默认实现将查询关键字基于 __ 进行拆分为键/值对,并试图找到资源查找与其 ORM 等效物之间的映射。

你的代码不应该仅仅在那里应用筛选器,而只是构建它。这里是一个改进和修复的版本:

def build_filters(self, filters=None):
    if filters is None:
        filters = {}
    orm_filters = super(BusinessResource, self).build_filters(filters)

    if('query' in filters):
        query = filters['query']
        qset = (
                Q(name__icontains=query) |
                Q(description__icontains=query) |
                Q(email__icontains=query)
                )
        orm_filters.update({'custom': qset})

    return orm_filters

def apply_filters(self, request, applicable_filters):
    if 'custom' in applicable_filters:
        custom = applicable_filters.pop('custom')
    else:
        custom = None

    semi_filtered = super(BusinessResource, self).apply_filters(request, applicable_filters)

    return semi_filtered.filter(custom) if custom else semi_filtered
由于您正在使用Q对象,标准的apply_filters方法无法智能地应用您的自定义过滤键(因为没有),但是您可以快速覆盖它并添加一个名为“custom”的特殊过滤器。 这样做可以让您的build_filters找到适当的过滤器,构造其含义,并将其作为自定义传递给apply_filters,后者将直接应用它,而不是尝试从字典中解包其值作为项。

3
字典没有“extend”方法。应该使用以下代码:orm_filters.update({'custom': qset}),其中更新字典键“custom”的值为变量“qset”。 - Karmo Rosental
1
这个解决方案会导致调用数据库两次(一次是为了半过滤,另一次是为了自定义过滤)。以下稍微不同的代码适用于我:如果'applicable_filters'中包含'custom',则将其弹出,返回Outreaches.objects.filter(custom);否则,返回super(OutreachResource, self).apply_filters(request, applicable_filters)。 - Israel Zalmanov

0

借鉴astevanovic的答案并稍作修改,以下代码更为简洁且可靠。

主要区别在于使用None作为键而非custom(可能与列名冲突),从而使apply_filters更加健壮。

def build_filters(self, filters=None):
    if filters is None:
        filters = {}
    orm_filters = super(BusinessResource, self).build_filters(filters)

    if 'query' in filters:
        query = filters['query']
        qset = (
                Q(name__icontains=query) |
                Q(description__icontains=query) |
                Q(email__icontains=query)
                )
        orm_filters.update({None: qset}) # None is used as the key to specify that these are non-keyword filters

    return orm_filters

def apply_filters(self, request, applicable_filters):
    return self.get_object_list(request).filter(*applicable_filters.pop(None, []), **applicable_filters)
    # Taking the non-keyword filters out of applicable_filters (if any) and applying them as positional arguments to filter()

0
我是这样解决这个问题的:
Class MyResource(ModelResource):

  def __init__(self, *args, **kwargs):
    super(MyResource, self).__init__(*args, **kwargs)
    self.q_filters = []

  def build_filters(self, filters=None):
    orm_filters = super(MyResource, self).build_filters(filters)

    q_filter_needed_1 = []
    if "what_im_sending_from_client" in filters:
      if filters["what_im_sending_from_client"] == "my-constraint":
        q_filter_needed_1.append("something to filter")

    if q_filter_needed_1:
      a_new_q_object = Q()
      for item in q_filter_needed:
        a_new_q_object = a_new_q_object & Q(filtering_DB_field__icontains=item)
      self.q_filters.append(a_new_q_object)

  def apply_filters(self, request, applicable_filters):
    filtered = super(MyResource, self).apply_filters(request, applicable_filters)

    if self.q_filters:
      for qf in self.q_filters:
        filtered = filtered.filter(qf)
      self.q_filters = []

    return filtered

这种方法感觉比我见过的其他方法更清晰地分离了关注点。


在资源实例上放置特定于请求的信息真的是一个非常糟糕的想法。因此,self.q_filters.append(a_new_q_object)。这是因为在部署有多个线程的环境中,您可能会发现一个请求的状态影响了另一个请求的状态。例如,一个请求中构建的所有过滤器实际上可以应用于完全不同的请求,这取决于时间安排。请参见此处的文档:http://django-tastypie.readthedocs.io/en/latest/resources.html#why-class-based 传递bundle对象解决了这个问题。 - Cameron Lee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接