【问题标题】:OR definition of filters when using relations in django filter在 django 过滤器中使用关系时定义过滤器
【发布时间】:2018-10-04 17:04:09
【问题描述】:

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

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

编辑2

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

 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

编辑3

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

编辑4

这个issue也有同样的问题。

【问题讨论】:

  • 将您的过滤器放置在 filters.py 文件中,而不是在 views.py 中。查看 django-filters 是否在 settings.py 中的 INSTALLED_APPS 列表中
  • PersonPersonSessionBilling 模型有什么关系?上面的代码看起来不完整。我可以从给定的信息中弥补的是,您将PersonSessionBilling(计费具有session 属性)和Person(假设branch_sessions 反向映射在过滤器中使用)相关联.尝试使用此人拥有的帐单进行过滤。像这样的东西应该可以工作 - start_time = django_filters.DateFromToRangeFilter(name='billings__sessions__start_time',distinct=True) 假设 Person 有一个属性 billing。让我知道这是否有帮助。
  • 对于模型中的问题我很抱歉我忘记了模型中的一些字段。 sessionperson 具有外键,billingsession 具有一对一的关系。使用这些假设,我认为我的过滤器没有问题,并且必须按预期工作,但事实并非如此。
  • @GeancarloMurillo 我不明白为什么更改过滤器实现的文件和位置会影响过滤器的结果?正如我解释的那样,我从过滤器中得到了一些结果,但这不是我所期望的。
  • 是 Django 文档推荐的关于更好地组织代码和良好实践技能的建议。 filters.py 中的过滤器,signals.py 中的信号,views.py 中的视图等等。抱歉,如果它不能解决您的问题。你能举一些例子说明你得到了什么以及你除了什么吗?

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


【解决方案1】:

我还没有解决办法;但我认为对问题的简明总结会在工作中比我的头脑更清晰!

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

  1. 您在相关模型上定义了两个离散过滤器这一事实;导致过滤器跨越多值关系
  2. FilterSet 实现过滤的方式

让我们更详细地看看这些:

过滤跨越多值关系

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

基本上,start_time 过滤器将.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 操作,它基本上采用查询集,然后向其中添加一个连续的.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})

所以这两个前提条件为这个问题创造了完美的风暴。

【讨论】:

  • 我很困惑,因为我的要求似乎是合乎逻辑的,但似乎没有人在 django 过滤器中没有这样的问题,这让我想到这可能是我的模型的定义或调整过滤器这样在 django 建模中是一种不良且罕见的行为。你认为这个问题的定义完全不同吗?
【解决方案2】:

这对我有用:

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() 调用中

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-08-17
    • 2016-07-01
    • 2016-12-26
    • 1970-01-01
    • 2018-03-15
    • 2015-08-08
    • 2021-04-29
    • 2014-01-30
    相关资源
    最近更新 更多