根据条件使用外键计数注释 Django 查询集
Annotating Django querysets with ForeignKey Counts subject to conditions
这是我的模型的简化版本:
class Airport(models.Model):
iata = models.CharField()
name = models.CharField()
latitude = models.FloatField()
longitude = models.FloatField()
class Flight(models.Model):
origin = models.ForeignKey('Airport', related_name='origins')
destination = models.ForeignKey('Airport', related_name='destinations')
owner = models.ForeignKey(User)
给定一个 User
,我想创建一个列表,其中包含出现在 origin
或 destination
字段中的所有 Airport
对象他拥有 19=] 个对象,每个对象都标有相应数量的 Flight
个对象。
例如,假设用户乘坐了 3 个航班:LAX-LHR
、LHR-CDG
和 CDG-JFK
。然后我想查询其中 returns 以下对象:
[LHR, id__count=2}, {CDG, id__count=2}, {LAX, id__count=1}, {JFK, id__count=1}]
在上面,三个字母代码代表Airport
个对象或其所有字段。
通常,可能有数千个 User
s 和数万个 Airport
s 和 Flight
s,所以我正在寻找比 for 的明显解决方案更有效的方法循环和 if 语句,最好在单个数据库查询中。
我目前的进度是这个查询:
Airport.objects.filter(
Q(origins__owner=user) | Q(destinations__owner=user)
)
.distinct()
.annotate(
id__count=Count('origins', distinct=True) + Count('destinations', distinct=True)
).order_by('-id__count')
这只适用于一个用户,因为最初的 filter
只保留那些出现在他的航班中某处的机场。但是当他们是多个用户时,它显然会失败,因为计数包括每个用户的航班。我需要一些方法来仅 Count
那些 Flight
服从某个 属性 的对象,即 owner=user
其中 user
是某个 User
对象。
编辑:阅读 this page in the Djnago documentation 后,似乎将过滤器放在首位应该可以根据需要进行这项工作。但事实并非如此,至少当我使用 Q 对象时是这样。我发现以下非常令人困惑的结果。
当我使用此查询时,即仅查看起点,然后它起作用,并且 num_origins
字段仅计算属于指定 user
:
的那些航班
Airport.objects.filter(origins__owner=user).annotate(num_origins=Count('origins'))
(这不完全是我需要的,因为计数仅包括起源于某个 Airport
的航班,但它确实正确过滤了 User
。)
但是,当我什么都不做,只是用两个 Q 对象替换单个过滤器时,结合或,即
Airport.objects.filter(Q(origins__owner=user) | Q(destinations__owner=user)).annotate(num_origins=Count('origins'))
现在它统计了属于每个用户的航班!似乎过滤器使用 Q 对象时的注释 "forgets"。这是怎么回事?
你能这样试试吗?我没有在 shell 上测试它,所以我不确定 'distinct_flights' 列表结构,但你会明白的。
# This is all of the distinct flights of your users.
distinct_flights = Flight.objects.filter(owner__in=[user1.id, user2.id]).distinct().values_list('origin','destination')
# This is all of the airports included in the flights above.
Airport.objects.filter(
Q(origins__in=distinct_flights['origin'])||
Q(destination__in=distinct_flights['destination'])
)
# The rest is annotation from those airports as you did before. You can annotate it on the above query again.
我想你可以用条件表达式来实现:
from django.db.models import Case, When
Airport.objects.filter(
Q(origins__owner=user) | Q(destinations__owner=user)
).annotate(
num_origins=Count(
Case(When(Q(origin__owner=user), then=1),output_field=CharField()),
),
num_destinations=Count(
Case(When(Q(destination__owner=user), then=1),output_field=CharField()),
)
)
请注意,When
子句重复您最初执行的相同筛选。相反,这样做实际上可能更有效(您可能需要检查生成的 SQL 查询以找出答案):
Airport.objects.annotate(
num_origins=Count(
Case(When(Q(origin__owner=user), then=1), output_field=CharField()),
),
num_destinations=Count(
Case(When(Q(destination__owner=user), then=1),output_field=CharField()),
)
).filter(Q(num_origins__gt=0) | Q(num_destinations__gt=0))
即注释所有航班,然后过滤掉计数为 0 的航班。
然后您可以在 Python 中添加 num_origins
和 num_destinations
。
如果您使用的是 Django 2,那么它仍然更简单,因为您可以将过滤器参数传递给 Count
:
Airport.objects.annotate(
num_origins=Count('origins', filter=Q(origin__owner=user), distinct=True),
num_destinations=Count('destinations', filter=Q(destination__owner=user), disctinct=True)
).filter(Q(num_origins__gt=0) | Q(num_destinations__gt=0))
这是我的模型的简化版本:
class Airport(models.Model):
iata = models.CharField()
name = models.CharField()
latitude = models.FloatField()
longitude = models.FloatField()
class Flight(models.Model):
origin = models.ForeignKey('Airport', related_name='origins')
destination = models.ForeignKey('Airport', related_name='destinations')
owner = models.ForeignKey(User)
给定一个 User
,我想创建一个列表,其中包含出现在 origin
或 destination
字段中的所有 Airport
对象他拥有 19=] 个对象,每个对象都标有相应数量的 Flight
个对象。
例如,假设用户乘坐了 3 个航班:LAX-LHR
、LHR-CDG
和 CDG-JFK
。然后我想查询其中 returns 以下对象:
[LHR, id__count=2}, {CDG, id__count=2}, {LAX, id__count=1}, {JFK, id__count=1}]
在上面,三个字母代码代表Airport
个对象或其所有字段。
通常,可能有数千个 User
s 和数万个 Airport
s 和 Flight
s,所以我正在寻找比 for 的明显解决方案更有效的方法循环和 if 语句,最好在单个数据库查询中。
我目前的进度是这个查询:
Airport.objects.filter(
Q(origins__owner=user) | Q(destinations__owner=user)
)
.distinct()
.annotate(
id__count=Count('origins', distinct=True) + Count('destinations', distinct=True)
).order_by('-id__count')
这只适用于一个用户,因为最初的 filter
只保留那些出现在他的航班中某处的机场。但是当他们是多个用户时,它显然会失败,因为计数包括每个用户的航班。我需要一些方法来仅 Count
那些 Flight
服从某个 属性 的对象,即 owner=user
其中 user
是某个 User
对象。
编辑:阅读 this page in the Djnago documentation 后,似乎将过滤器放在首位应该可以根据需要进行这项工作。但事实并非如此,至少当我使用 Q 对象时是这样。我发现以下非常令人困惑的结果。
当我使用此查询时,即仅查看起点,然后它起作用,并且 num_origins
字段仅计算属于指定 user
:
Airport.objects.filter(origins__owner=user).annotate(num_origins=Count('origins'))
(这不完全是我需要的,因为计数仅包括起源于某个 Airport
的航班,但它确实正确过滤了 User
。)
但是,当我什么都不做,只是用两个 Q 对象替换单个过滤器时,结合或,即
Airport.objects.filter(Q(origins__owner=user) | Q(destinations__owner=user)).annotate(num_origins=Count('origins'))
现在它统计了属于每个用户的航班!似乎过滤器使用 Q 对象时的注释 "forgets"。这是怎么回事?
你能这样试试吗?我没有在 shell 上测试它,所以我不确定 'distinct_flights' 列表结构,但你会明白的。
# This is all of the distinct flights of your users.
distinct_flights = Flight.objects.filter(owner__in=[user1.id, user2.id]).distinct().values_list('origin','destination')
# This is all of the airports included in the flights above.
Airport.objects.filter(
Q(origins__in=distinct_flights['origin'])||
Q(destination__in=distinct_flights['destination'])
)
# The rest is annotation from those airports as you did before. You can annotate it on the above query again.
我想你可以用条件表达式来实现:
from django.db.models import Case, When
Airport.objects.filter(
Q(origins__owner=user) | Q(destinations__owner=user)
).annotate(
num_origins=Count(
Case(When(Q(origin__owner=user), then=1),output_field=CharField()),
),
num_destinations=Count(
Case(When(Q(destination__owner=user), then=1),output_field=CharField()),
)
)
请注意,When
子句重复您最初执行的相同筛选。相反,这样做实际上可能更有效(您可能需要检查生成的 SQL 查询以找出答案):
Airport.objects.annotate(
num_origins=Count(
Case(When(Q(origin__owner=user), then=1), output_field=CharField()),
),
num_destinations=Count(
Case(When(Q(destination__owner=user), then=1),output_field=CharField()),
)
).filter(Q(num_origins__gt=0) | Q(num_destinations__gt=0))
即注释所有航班,然后过滤掉计数为 0 的航班。
然后您可以在 Python 中添加 num_origins
和 num_destinations
。
如果您使用的是 Django 2,那么它仍然更简单,因为您可以将过滤器参数传递给 Count
:
Airport.objects.annotate(
num_origins=Count('origins', filter=Q(origin__owner=user), distinct=True),
num_destinations=Count('destinations', filter=Q(destination__owner=user), disctinct=True)
).filter(Q(num_origins__gt=0) | Q(num_destinations__gt=0))