我有三个简单关系的模型如下:
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
)
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
这是我的示例中的查询所使用的数据并使用它们,您可以看到我在上面提到的查询中必须返回的内容:
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
我尝试使用prefetch_related
连接表并获得我预期的结果,因为我认为额外的连接会导致此问题,但这不起作用,我仍然得到相同的结果,这没有任何影响。
此issue存在同样的问题。
答案 0 :(得分:1)
据我理解;你的核心问题是两个前提条件的结果:
FilterSet
实现过滤的方式这是一个很好的资源,可以更好地了解问题前提条件#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()
调用中