Here is a simplified version of my models:
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)
Given a User
, I would like to create a list of all the Airport
objects that appear in either the origin
or destination
fields of the Flight
objects he owns, each annotated with the corresponding number of Flight
objects.
For example, suppose a user has been on 3 flights: LAX-LHR
, LHR-CDG
, and CDG-JFK
. Then I would like a query which returns the following object:
[LHR, id__count=2}, {CDG, id__count=2}, {LAX, id__count=1}, {JFK, id__count=1}]
In the above, the three letter codes stand for Airport
objects or all their fields.
Generally, there may be thousands of User
s and tens of thousands of Airport
s and Flight
s, so I am seeking something more efficient than the obvious solution with for loops and if statements, preferably in a single database query.
My current progress is this query:
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')
This works perfectly with only one user, because the initial filter
only keeps those airports which appear somewhere in his flights. But it clearly fails when their are multiple users, because the counts include every user's flights. I need some way to only Count
those Flight
objects which obey a certain property, namely owner=user
where user
is a certain User
object.
Edit: after reading this page in the Djnago documentation, it seems that putting the filter first should make this work as needed. But it doesn't, at least when I use Q objects. I've found the following highly confusing result.
When I use this query, i.e. only looking at the origins, then it works, and the num_origins
field counts only those flights belonging to the specified user
:
Airport.objects.filter(origins__owner=user).annotate(num_origins=Count('origins'))
(This isn't exactly what I need because the counts only include flights whose origin is a certain Airport
, but it does filter the User
s correctly.)
But, when I do nothing but replace a single filter with two Q objects combined with or, i.e.
Airport.objects.filter(Q(origins__owner=user) | Q(destinations__owner=user)).annotate(num_origins=Count('origins'))
now it counts flights belonging to every user! It seems that the annotate "forgets" about the filter when it uses Q objects. What is going on here?