I am trying to make a very simple Subquery
that uses OuterRef
(not for practical purposes, just to get it working), but keep running into same error.
posts/models.py
code
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=120)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=120)
tags = models.ManyToManyField(Tag)
def __str__(self):
return self.title
manage.py shell
code
>>> from django.db.models import OuterRef, Subquery
>>> from posts.models import Tag, Post
>>> tag1 = Tag.objects.create(name='tag1')
>>> post1 = Post.objects.create(title='post1')
>>> post1.tags.add(tag1)
>>> Tag.objects.filter(post=post1.pk)
<QuerySet [<Tag: tag1>]>
>>> tags_list = Tag.objects.filter(post=OuterRef('pk'))
>>> Post.objects.annotate(count=Subquery(tags_list.count()))
The last two lines should give me number of tags for each Post object. And here I keep getting the same error:
ValueError: This queryset contains a reference to an outer query and may only be used in a subquery.
The django-sql-utils package makes this kind of subquery aggregation simple. Just
pip install django-sql-utils
and then:The API for SubqueryCount is the same as Count, but it generates a subselect in the SQL instead of joining to the related table.
One of the problems with your example is that you cannot use
queryset.count()
as a subquery, because.count()
tries to evaluate the queryset and return the count.So one may think that the right approach would be to use
Count()
instead. Maybe something like this:This wont work for two reasons:
The
Tag
queryset selects allTag
fields, whileCount
can only count on one field. Thus:Tag.objects.filter(post=OuterRef('pk')).only('pk')
is needed (to select counting ontag.pk
).Count
itself is not aSubquery
class,Count
is anAggregate
. So the expression generated byCount
is not recognized as aSubquery
(OuterRef
requires subquery), we can fix that by usingSubquery
.Apply-ing fixes for 1) and 2) would produce:
However if you inspect the query being produced
You may notice that we have a
GROUP BY
clause. This is because Count is an Aggregate, right now it does not affect the result, but in some other cases it may. Thats why the docs suggest a little bit different approach, where the aggregation is moved into thesubquery
via a specific combination ofvalues
+annotate
+values
Finally this will produce: