2017-05-03 33 views
8

ich eine sehr einfachen Unterabfrage zu machen versuchen, die OuterRef verwendet (nicht für praktische Zwecke, nur um es in Betrieb), sondern hält in gleiche Fehler ausgeführt wird.Einfacher Subquery mit OuterRef

Beiträge/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 

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())) 

Die letzten beiden Zeilen Anzahl der Tags für jedes Objekt Beitrag geben Sie mir sollte. Und hier halte ich den gleichen Fehler:

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

Antwort

17

Eines der Probleme mit Ihrem Beispiel ist, dass Sie nicht queryset.count() als Unterabfrage verwenden können, weil .count() versucht, die queryset zu bewerten und die Zählung zurück.

So kann man denken, dass der richtige Ansatz Count() zu verwenden, anstatt wäre. Vielleicht so etwas wie folgt aus:

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

Dies wird nicht funktionieren zwar aus zwei Gründen:

  1. Die Tag queryset wählt alle Tag Felder, während Count nur auf einem Feld zählen kann. Also: Tag.objects.filter(post=OuterRef('pk')).only('pk') wird benötigt (zum Zählen auf tag.pk).

  2. Count selbst ist kein Subquery Klasse, Count ein Aggregate ist. So ist der von Count erzeugte Ausdruck wird nicht als Subquery erkannt, können wir das Problem beheben, indem Subquery verwenden.

Und die letzte Version wäre:

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

jedoch , wenn Sie inspizieren die Abfrage

produziert
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" 

Sie können feststellen, dass wir eine GROUP BY Klausel haben. Das liegt daran, dass Count ein Aggregat ist. Im Moment wirkt sich dies nicht auf das Ergebnis aus, in einigen anderen Fällen jedoch. Das ist, warum die docs ein wenig anderen Ansatz vorschlagen, wo die Aggregation in die subquery über eine spezifische Kombination von values + annotate + values

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

bewegt wird schließlich das erzeugt:

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" 
+0

Danke, dass hat funktioniert! Wenn ich jedoch 'pk__in = [1,2]' zum Tag-Filter hinzufüge, bekomme ich 'django.core.exceptions.FieldError: Expression enthält gemischte Typen. Sie müssen Ausgabefeld setzen. – mjuk

+1

Sie können versuchen, den 'queryset.query' zu drucken, und führen Sie es in Ihrem' RDBMS' direkt zu sehen, was man bekommt. Ich denke, dass für einige Zeilen 'Count' zurückgeben' NULL' statt 0 können Sie versuchen, zu bestätigen, dass durch temporäre ausschließen Reihen w/o Zahl, das heißt '.filter (count__gte = 1)'."Subquery" akzeptiert jedoch ein zweites Argument, das 'output_field' ist, das Sie versuchen können, es zu setzen:' output_field = fields.IntegerField() ' – Todor

+0

Danke, das ist genau das, was ich brauchte. – mjuk