Reputation:
I have these models:
class IceCream(models.Model):
name = models.CharField(max_length=255)
class Topping(models.Model):
name = models.CharField(max_length=255)
ice_cream = models.ForeignKey(IceCream, on_delete=models.CASCADE, related_name="toppings")
is_active = models.BooleanField(db_index=True)
I would like to run the following query to get a list of all ice creams, together with a count of the total number of available toppings, which in SQL would look like this:
SELECT ice_cream.*,
(SELECT COUNT(id) FROM topping WHERE topping.ice_cream = ice_cream.id AND topping.is_active = True) AS total_toppings
FROM ice_cream
This is a simplified version and there are more parameters in there, but if I can make this work, then things should work out. Can I do this in Django using Subquery expressions? I have not managed to make it work. Or are raw queries my only way out here because of the COUNT that I want to include in the subquery?
Upvotes: 0
Views: 951
Reputation: 1967
The django-sql-utils package makes this easy, pip install django-sql-utils
and then
from sql_util.utils import SubqueryCount
queryset = IceCream.objects.all().annotate(
total_toppings=SubqueryCount('toppings', filter=Q(is_active=True))
)
Upvotes: 3
Reputation: 41041
You can use annotate
for this, which supports aggregation functions like Count
. You can also add filters to aggregation functions by providing a Q
object.
# ...
from django.db.models import Count, Q
# ...
qs = IceCream.objects.all().annotate(
active_toppings_count=Count("toppings", filter=Q(toppings__is_active=True))
)
for ice_cream in qs:
print(ice_cream.name, ice_cream.active_toppings_count)
This uses a Left (outer) join to produce the results. This is (likely) better for performance than using a subquery.
If you must use a subquery, you can do the following:
from django.db.models import F, Func, OuterRef, Subquery
#...
subq = (
Topping.objects.filter(is_active=True, ice_cream=OuterRef("pk"))
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
qs = IceCream.objects.annotate(active_toppings_count=Subquery(subq))
Then the resulting SQL is equivalent to your raw SQL example:
SELECT "myapp_icecream"."id",
"myapp_icecream"."name",
(SELECT Count(U0."id") AS "count"
FROM "myapp_topping" U0
WHERE ( U0."ice_cream_id" = ( "myapp_icecream"."id" )
AND U0."is_active" )) AS "active_toppings_count"
FROM "myapp_icecream"
Upvotes: 4