或者在django过滤器中使用关系时定义过滤器

时间:2018-04-24 12:23:56

标签: django django-rest-framework django-filter django-filters

我有三个简单关系的模型如下:

models.py

class Person(models.Model):
    first_name = models.CharField(max_length=20)
    last_name = models.CharField(max_length=20)

class PersonSession(models.Model):
    start_time = models.DateTimeField(auto_now_add=True)
    end_time = models.DateTimeField(null=True,
                                    blank=True)
    person = models.ForeignKey(Person, related_name='sessions')

class Billing(models.Model):
    DEBT = 'DE'
    BALANCED = 'BA'
    CREDIT = 'CR'

    session = models.OneToOneField(PersonSession,
                                   blank=False,
                                   null=False,
                                   related_name='billing')
    STATUS = ((BALANCED, 'Balanced'),
              (DEBT, 'Debt'),
              (CREDIT, 'Credit'))

    status = models.CharField(max_length=2,
                              choices=STATUS,
                              blank=False,
                              default=BALANCED
                              )

views.py

class PersonFilter(django_filters.FilterSet):
    start_time = django_filters.DateFromToRangeFilter(name='sessions__start_time',
                                 distinct=True)
    billing_status = django_filters.ChoiceFilter(name='sessions__billing__status',
                        choices=Billing.STATUS,
                        distinct=True)

    class Meta:
        model = Person
        fields = ('first_name', 'last_name')

class PersonList(generics.ListCreateAPIView):
    queryset = Person.objects.all()
    serializer_class = PersonSerializer
    filter_backends = (django_filters.rest_framework.DjangoFilterBackend)
    filter_class = PersonFilter

我希望从人员终端获取账单,该账单在结算中具有DE状态且在一段时间内:

api/persons?start_time_0=2018-03-20&start_time_1=2018-03-23&billing_status=DE

但结果并非我所寻找的,这会使所有人在该时段内有一个会话并且具有DE状态的结算,无论该结算是否在该期间。

换句话说,似乎在两个过滤器字段之间使用or操作,我认为this post与此问题有关,但目前我无法找到获得我想要的结果的方法。我正在使用djang 1.10.3。

修改

我尝试写一个example来显示我需要什么以及从django过滤器得到什么。如果我在示例中使用以下查询的人,我只有两个人:

select * 
from 
test_filter_person join test_filter_personsession on test_filter_person.id=test_filter_personsession.person_id join test_filter_billing on test_filter_personsession.id=test_filter_billing.session_id 
where
start_time > '2000-02-01' and start_time < '2000-03-01' and status='DE';

这让我只是第1和第2人。但是如果我得到的东西与url类似,我会得到所有的人,类似的url(至少有一个我期望的相同)如下:

http://address/persons?start_time_0=2000-02-01&start_time_1=2000-03-01&billing_status=DE

EDIT2

这是我的示例中的查询所使用的数据并使用它们,您可以看到我在上面提到的查询中必须返回的内容:

 id | first_name | last_name | id |        start_time         |         end_time          | person_id | id | status | session_id 
----+------------+-----------+----+---------------------------+---------------------------+-----------+----+--------+------------
  0 | person     | 0         |  0 | 2000-01-01 16:32:00+03:30 | 2000-01-01 17:32:00+03:30 |         0 |  0 | DE     |          0
  0 | person     | 0         |  1 | 2000-02-01 16:32:00+03:30 | 2000-02-01 17:32:00+03:30 |         0 |  1 | BA     |          1
  0 | person     | 0         |  2 | 2000-03-01 16:32:00+03:30 | 2000-03-01 17:32:00+03:30 |         0 |  2 | DE     |          2
  1 | person     | 1         |  3 | 2000-01-01 16:32:00+03:30 | 2000-01-01 17:32:00+03:30 |         1 |  3 | BA     |          3
  1 | person     | 1         |  4 | 2000-02-01 16:32:00+03:30 | 2000-02-01 17:32:00+03:30 |         1 |  4 | DE     |          4
  1 | person     | 1         |  5 | 2000-03-01 16:32:00+03:30 | 2000-03-01 17:32:00+03:30 |         1 |  5 | DE     |          5
  2 | person     | 2         |  6 | 2000-01-01 16:32:00+03:30 | 2000-01-01 17:32:00+03:30 |         2 |  6 | DE     |          6
  2 | person     | 2         |  7 | 2000-02-01 16:32:00+03:30 | 2000-02-01 17:32:00+03:30 |         2 |  7 | DE     |          7
  2 | person     | 2         |  8 | 2000-03-01 16:32:00+03:30 | 2000-03-01 17:32:00+03:30 |         2 |  8 | BA     |          8

EDIT3

我尝试使用prefetch_related连接表并获得我预期的结果,因为我认为额外的连接会导致此问题,但这不起作用,我仍然得到相同的结果,这没有任何影响。

Edit4

issue存在同样的问题。

2 个答案:

答案 0 :(得分:1)

我还没有解决方案;但我认为对这个问题的简明总结将会比我的工作更多更好的思想!

据我理解;你的核心问题是两个前提条件的结果:

  1. 您在相关模型上定义了两个离散过滤器;导致过滤器跨越 - 多值关系
  2. FilterSet实现过滤的方式
  3. 让我们更详细地看一下这些:

    过滤器跨越多值关系

    这是一个很好的资源,可以更好地了解问题前提条件#1: https://docs.djangoproject.com/en/2.0/topics/db/queries/#spanning-multi-valued-relationships

    基本上,start_time过滤器会在您的Queryset中添加.filter(sessions__start_time=value)billing_status过滤器会向过滤器添加.filter(sessions_billing_status=value)。这导致上述“跨越 - 多值关系”问题,这意味着它将在这些过滤器之间执行OR,而不是AND,因为您需要它。

    这让我想到,为什么我们在start_time过滤器中看不到相同的问题;但这里的诀窍是它被定义为DateFromToRangeFilter;它在内部使用带有__range=构造的单个过滤器查询。如果它改为sessions__start_time__gt=sessions__start_time__lt=,我们会遇到同样的问题。

    FilterSet实现过滤的方式

      谈话很便宜;告诉我代码

    @property
    def qs(self):
        if not hasattr(self, '_qs'):
            if not self.is_bound:
                self._qs = self.queryset.all()
                return self._qs
    
            if not self.form.is_valid():
                if self.strict == STRICTNESS.RAISE_VALIDATION_ERROR:
                    raise forms.ValidationError(self.form.errors)
                elif self.strict == STRICTNESS.RETURN_NO_RESULTS:
                    self._qs = self.queryset.none()
                    return self._qs
                # else STRICTNESS.IGNORE...  ignoring
    
            # start with all the results and filter from there
            qs = self.queryset.all()
            for name, filter_ in six.iteritems(self.filters):
                value = self.form.cleaned_data.get(name)
    
                if value is not None:  # valid & clean data
                    qs = filter_.filter(qs, value)
    
            self._qs = qs
    
        return self._qs
    

    正如您所看到的,qs属性通过迭代Filter个对象列表来解析,连续传递初始qs并返回结果。见qs = filter_.filter(qs, value)

    此处的每个Filter对象都定义了一个特定的def filter操作,它基本上采用了Queryset,然后向其中添加了一个.filter

    以下是BaseFilter

    的示例
       def filter(self, qs, value):
            if isinstance(value, Lookup):
                lookup = six.text_type(value.lookup_type)
                value = value.value
            else:
                lookup = self.lookup_expr
            if value in EMPTY_VALUES:
                return qs
            if self.distinct:
                qs = qs.distinct()
            qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
            return qs
    

    重要的代码行是:qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})

    因此,这两个先决条件为这个问题创造了完美的风暴。

答案 1 :(得分:0)

这对我有用:

class FooFilterSet(FilterSet):

    def filter_queryset(self, queryset):
        """
        Overrides the basic methtod, so that instead of iterating over tthe queryset with multiple `.filter()`
        calls, one for each filter, it accumulates the lookup expressions and applies them all in a single
        `.filter()` call  - to filter with an explicit "AND" in many to many relationships.
        """
        filter_kwargs = {}
        for name, value in self.form.cleaned_data.items():
            if value not in EMPTY_VALUES:
                lookup = '%s__%s' % (self.filters[name].field_name, self.filters[name].lookup_expr)
                filter_kwargs.update({lookup:value})

        queryset = queryset.filter(**filter_kwargs)
        assert isinstance(queryset, models.QuerySet), \
            "Expected '%s.%s' to return a QuerySet, but got a %s instead." \
            % (type(self).__name__, name, type(queryset).__name__)
        return queryset

覆盖filter_queryset方法,以便它累积表达式并将它们应用于单个.filter()调用中