Subconsulta simple con OuterRef

Estoy tratando de hacer una Subconsulta muy simple que use OuterRef (no con fines prácticos, solo para que funcione), pero me mantengo en el mismo error.

posts / models.py

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 

código de shell manage.py

 >>> 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 []> >>> tags_list = Tag.objects.filter(post=OuterRef('pk')) >>> Post.objects.annotate(count=Subquery(tags_list.count())) 

Las dos últimas líneas deberían indicarme el número de tags para cada objeto Post. Y aquí sigo recibiendo el mismo error:

 ValueError: This queryset contains a reference to an outer query and may only be used in a subquery. 

Uno de los problemas con su ejemplo es que no puede usar queryset.count() como una subconsulta, porque .count() intenta evaluar el queryset y devolver el conteo.

Entonces, uno puede pensar que el enfoque correcto sería usar Count() lugar. Tal vez algo como esto:

 Post.objects.annotate( count=Count(Tag.objects.filter(post=OuterRef('pk'))) ) 

Esto no funcionará por dos razones:

  1. El conjunto de consultas de Tag selecciona todos los campos de Tag , mientras que el Count solo puede contar en un campo. Por lo tanto: Tag.objects.filter(post=OuterRef('pk')).only('pk') se necesita ( Tag.objects.filter(post=OuterRef('pk')).only('pk') (para seleccionar el conteo en tag.pk ).

  2. Count sí no es una clase de Subquery , Count es un Aggregate . Por lo tanto, la expresión generada por Count no se reconoce como una Subquery , podemos corregirla utilizando la Subquery .

Aplicar arreglos para 1) y 2) produciría:

 Post.objects.annotate( count=Count(Subquery(Tag.objects.filter(post=OuterRef('pk')).only('pk'))) ) 

Sin embargo si inspeccionas la consulta que se está produciendo

 SELECT "tests_post"."id", "tests_post"."title", COUNT((SELECT U0."id" FROM "tests_tag" U0 INNER JOIN "tests_post_tags" U1 ON (U0."id" = U1."tag_id") WHERE U1."post_id" = ("tests_post"."id")) ) AS "count" FROM "tests_post" GROUP BY "tests_post"."id", "tests_post"."title" 

Puede notar que tenemos una cláusula GROUP BY . Esto se debe a que Count es un Agregado, en este momento no afecta el resultado, pero en otros casos puede que lo haga. Es por eso que los documentos sugieren un enfoque un poco diferente, donde la agregación se mueve a la subquery través de una combinación específica de values + annotate + values

 Post.objects.annotate( count=Subquery( Tag.objects.filter(post=OuterRef('pk')) # The first .values call defines our GROUP BY clause # Its important to have a filtration on every field defined here # Otherwise you will have more than one group per row!!! # This will lead to subqueries to return more than one row! # But they are not allowed to do that! # In our example we group only by post # and we filter by post via OuterRef .values('post') # Here we say: count how many rows we have per group .annotate(count=Count('pk')) # Here we say: return only the count .values('count') ) ) 

Finalmente esto producirá:

 SELECT "tests_post"."id", "tests_post"."title", (SELECT COUNT(U0."id") AS "count" FROM "tests_tag" U0 INNER JOIN "tests_post_tags" U1 ON (U0."id" = U1."tag_id") WHERE U1."post_id" = ("tests_post"."id") GROUP BY U1."post_id" ) AS "count" FROM "tests_post"