根据条件使用 ForeignKey Counts 注释 Django 查询集
Posted
技术标签:
【中文标题】根据条件使用 ForeignKey Counts 注释 Django 查询集【英文标题】:Annotating Django querysets with ForeignKey Counts subject to conditions 【发布时间】:2018-05-29 02:45:37 【问题描述】:这是我的模型的简化版本:
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
,我想创建一个所有Airport
对象的列表,这些对象出现在他拥有的Flight
对象的origin
或destination
字段中,每个对象都用相应的注释Flight
对象的数量。
例如,假设用户已乘坐 3 个航班:LAX-LHR
、LHR-CDG
和 CDG-JFK
。然后我想要一个返回以下对象的查询:
[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
s。)
但是,当我只用两个 Q 对象结合或替换单个过滤器时,即
Airport.objects.filter(Q(origins__owner=user) | Q(destinations__owner=user)).annotate(num_origins=Count('origins'))
现在它计算属于每个用户的航班!似乎注释在使用 Q 对象时“忘记了”过滤器。这是怎么回事?
【问题讨论】:
为什么没有为 Flight 对象的所有者键添加相关名称?然后,您可以获取所有用户的航班并对其应用 distinct 以便仅计算不同的航班。 @ÇağatayBarın 我已经为所有者字段添加了一个相关名称,但我仍然看不到如何使用它。你能再解释一下吗?谢谢。 【参考方案1】:你可以这样试试吗?我没有在 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.
【讨论】:
谢谢,但这种结构会计算每个用户的航班。当我不在过滤器中使用 Q 对象时,它可以工作,并且注释只计算指定用户的航班。但是当我使用 Q 对象与 or 结合使用时,注释会计算每个用户的航班。查看我的编辑。【参考方案2】:我认为您可以使用条件表达式来实现这一点:
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))
【讨论】:
感谢您的回答,但我无法获得第一个工作选项。我认为您实际上是指default=0
而不是else=0
,但即便如此,计数也是错误的,并且似乎忽略了用户过滤器。这可能是数据库问题吗?我目前正在使用 SQLite,我打算继续使用它,但如果有帮助,我可以迁移到 mysql。无论如何,接下来我将尝试升级到 Django 2.0 并使用您的第二个选项。
我升级到 Django 2.0 并且您的第二个选项有效!不过有几个错别字:Count
的第一个参数应该是'origins'
和'destinations'
,我们还必须包括distinct=True
。如果您纠正这些错误,我会接受您的回答。
我根据您的 cmets 进行了编辑。不太清楚为什么第一种方法不起作用。请注意,即使您的代码有效,也不建议在生产环境中使用 SQLite。最好使用 Postgres 或 MySQL。以上是关于根据条件使用 ForeignKey Counts 注释 Django 查询集的主要内容,如果未能解决你的问题,请参考以下文章
可以在 Django 中执行条件 ForeignKey.on_delete 吗?
如何在 Python 中手动排列 Value_counts 的索引
Streamlit - 将 value_counts / groupby 应用于运行时选择的列