根据条件使用外键计数注释 Django 查询集

jc3*_*315 3 python django django-models django-queryset django-aggregation

这是我的模型的简化版本:

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)
Run Code Online (Sandbox Code Playgroud)

给定 a User,我想创建一个Airport包含出现在他拥有的对象的origindestination字段中的所有对象的列表Flight,每个对象都用相应数量的Flight对象进行注释。

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

[LHR, id__count=2}, {CDG, id__count=2}, {LAX, id__count=1}, {JFK, id__count=1}]
Run Code Online (Sandbox Code Playgroud)

在上面,三个字母代码代表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')
Run Code Online (Sandbox Code Playgroud)

这仅适用于一个用户,因为最初filter只保留那些出现在他的航班某处的机场。但是当他们有多个用户时,它显然会失败,因为计数包括每个用户的航班。我只需要一些方法来处理Count那些Flight服从某个属性的对象,即某个对象owner=user在哪里。userUser


编辑:在Djnago 文档中阅读此页面后,似乎将过滤器放在首位应该可以根据需要进行这项工作。但它没有,至少当我使用 Q 对象时。我发现以下非常令人困惑的结果。

当我使用这个查询时,即只查看起源,然后它就起作用了,并且该num_origins字段只计算属于指定的那些航班user

Airport.objects.filter(origins__owner=user).annotate(num_origins=Count('origins'))
Run Code Online (Sandbox Code Playgroud)

(这并不完全是我需要的,因为计数只包括原点为某个的航班Airport,但它确实User正确过滤了s。)

但是,当我什么都不做,只是用两个 Q 对象组合或替换单个过滤器时,即

Airport.objects.filter(Q(origins__owner=user) | Q(destinations__owner=user)).annotate(num_origins=Count('origins'))
Run Code Online (Sandbox Code Playgroud)

现在它计算属于每个用户的航班!似乎注释在使用 Q 对象时“忘记”了过滤器。这里发生了什么?

sol*_*oke 5

我认为您可以使用条件表达式来实现这一点:

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()),
    )
)
Run Code Online (Sandbox Code Playgroud)

请注意,该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))
Run Code Online (Sandbox Code Playgroud)

即,注释所有航班,然后过滤掉计数为 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))
Run Code Online (Sandbox Code Playgroud)