根据条件使用 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 对象的origindestination 字段中,每个对象都用相应的注释Flight 对象的数量。

例如,假设用户已乘坐 3 个航班:LAX-LHRLHR-CDGCDG-JFK。然后我想要一个返回以下对象的查询:

[LHR, id__count=2, CDG, id__count=2, LAX, id__count=1, JFK, id__count=1]

在上面,三个字母代码代表Airport 对象或其所有字段。

通常,可能有数千个 Users 和数万个 Airports 和 Flights,所以我正在寻找比 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 的航班,但它确实过滤了Users。)

但是,当我只用两个 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_originsnum_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输入与循环

如何在 Python 中手动排列 Value_counts 的索引

Streamlit - 将 value_counts / groupby 应用于运行时选择的列

BigQuery - 具有不同 WHERE 参数的嵌套查询?

Django:条件表达式