使用ForeignKey Counts注释Django查询集受条件限制

时间:2017-12-14 22:24:55

标签: 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)

鉴于User,我想创建Airport origindestination字段中显示的所有Flight个对象的列表他拥有的对象,每个对象都带有相应数量的Flight个对象。

例如,假设用户使用了3个航班:LAX-LHRLHR-CDGCDG-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。)

但是,当我什么也没做,只用两个Q对象替换单个过滤器,或者

Airport.objects.filter(Q(origins__owner=user) | Q(destinations__owner=user)).annotate(num_origins=Count('origins'))

现在它统计属于每个用户的航班!看来,当使用Q对象时,注释会“忘记”过滤器。这是怎么回事?

2 个答案:

答案 0 :(得分:2)

I think you can achieve this with conditional expressions:

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, else=0)),
    ),
    num_destinations=Count(
        Case(When(Q(destination__owner=user), then=1, else=0)),
    )
)

Note that the When clause is repeating the same filter that you do initially. It might actually be more efficient to do this instead (you probably need to inspect the resulting SQL query to find out):

Airport.objects.annotate(
    num_origins=Count(
        Case(When(Q(origin__owner=user), then=1, else=0)),
    ),
    num_destinations=Count(
        Case(When(Q(destination__owner=user), then=1, else=0)),
    )
).filter(Q(num_origins__gt=0) | Q(num_destinations__gt=0))

i.e., annotate all flights, and then filter out the ones where the count was 0.

You can then add up num_origins and num_destinations in Python.

If you are using Django 2, then it is simpler still because you can pass a filter argument to 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))

答案 1 :(得分:0)

你能尝试这样吗?我没有在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.