2017-05-03 16 views
8

Próbuję zrobić bardzo proste Podkwerendę, która używa OuterRef (nie w celach praktycznych, tylko po to, aby działało), ale nadal działają na ten sam błąd.Proste podkwerendy z OuterRef

postów/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 

kod manage.py shell

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

Ostatnie dwie linie powinny dać mi liczbę tagów dla każdego obiektu post. A tu wciąż otrzymuję ten sam błąd:

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

Odpowiedz

17

Jednym z problemów ze swoim przykładzie jest to, że nie można używać queryset.count() jako podzapytania, ponieważ .count() próbuje ocenić queryset i powrót licznika.

Można więc pomyśleć, że właściwym podejściem byłoby zamiast tego użycie Count(). Może coś takiego:

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

to przyzwyczajenie praca dla dwóch powodów:

  1. Tag queryset wybiera wszystkie Tag pola, natomiast Count może liczyć tylko na jednym polu. Tak więc: potrzebne jest Tag.objects.filter(post=OuterRef('pk')).only('pk') (aby wybrać zliczanie na tag.pk).

  2. sama Count nie jest klasa Subquery, Count jest Aggregate. Tak więc wyrażenie generowane przez Count nie jest rozpoznawane jako Subquery, możemy to naprawić, używając Subquery.

A ostateczna wersja będzie:

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

Jednak jeśli skontrolować zapytanie produkowane

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" 

Można zauważyć, że mamy GROUP BY klauzuli. Dzieje się tak, ponieważ Count jest agregatem, obecnie nie ma wpływu na wynik, ale w niektórych innych przypadkach może. Ów dlaczego docs sugerują nieco inne podejście, w którym agregacja jest przemieszczana do subquery za pomocą specyficznej kombinacji values + annotate + values

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

Wreszcie będzie to produkują:

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

Dzięki, że pracował! Jednak gdy dodaję 'pk__in = [1,2]' do filtra Tag, otrzymuję 'django.core.exceptions.FieldError: Expression zawiera typy mieszane. Musisz ustawić output_field'. – mjuk

+1

Możesz spróbować wydrukować 'queryset.query' i wykonać je w swoim' RDBMS' bezpośrednio, aby zobaczyć, co otrzymasz w zamian. Sądzę, że dla niektórych wierszy 'Count' może zwracać' NULL' zamiast 0. Możesz spróbować potwierdzić, że przez tymczasowe wykluczenie wierszy bez liczenia, tj. '.filter (count__gte = 1)'.Jednak "Podzaprocesowanie" akceptuje drugi argument, który jest "output_field", możesz spróbować ustawić go na: 'output_field = fields.IntegerField()' – Todor

+0

dzięki, to dokładnie to, czego potrzebowałem. – mjuk