web-dev-qa-db-fra.com

Comment annoter Count avec une condition dans un Django queryset

En utilisant Django ORM, peut-on faire quelque chose comme queryset.objects.annotate(Count('queryset_objects', gte=VALUE)). Catch my drift?


Voici un exemple rapide à utiliser pour illustrer une réponse possible:

Sur un site Web Django, les créateurs de contenu soumettent des articles et les utilisateurs habituels les regardent (c'est-à-dire les lisent). Les articles peuvent être publiés (c'est-à-dire accessibles à tous), ou en mode brouillon. Les modèles décrivant ces exigences sont:

class Article(models.Model):
    author = models.ForeignKey(User)
    published = models.BooleanField(default=False)

class Readership(models.Model):
    reader = models.ForeignKey(User)
    which_article = models.ForeignKey(Article)
    what_time = models.DateTimeField(auto_now_add=True)

Ma question est: Comment puis-je obtenir tous les articles publiés, triés par lectorat unique des 30 dernières minutes? C'est à dire. Je souhaite compter le nombre de vues distinctes (uniques) que chaque article publié a reçues au cours de la dernière demi-heure, puis créer une liste d'articles triés par ces vues distinctes.


J'ai essayé:

date = datetime.now()-timedelta(minutes=30)
articles = Article.objects.filter(published=True).extra(select = {
  "views" : """
  SELECT COUNT(*)
  FROM myapp_readership
    JOIN myapp_article on myapp_readership.which_article_id = myapp_article.id
  WHERE myapp_readership.reader_id = myapp_user.id
  AND myapp_readership.what_time > %s """ % date,
}).order_by("-views")

Cela a généré l'erreur suivante: erreur de syntaxe égale ou proche de "01" (où "01" était l'objet datetime dans extra). Ce n'est pas grand chose à faire.

47
Hassan Baig

Pour Django> = 1.8

Utilisez agrégation conditionnelle :

from Django.db.models import Count, Case, When, IntegerField
Article.objects.annotate(
    numviews=Count(Case(
        When(readership__what_time__lt=treshold, then=1),
        output_field=IntegerField(),
    ))
)

Explication: la requête normale dans vos articles sera annotée avec le champ numviews. Ce champ sera construit comme une expression CASE/WHEN, entourée de Count, qui retournera 1 pour les critères de correspondance de lectorat et NULL pour les critères de lectorat ne correspondant pas. Count ignorera les valeurs NULL et ne comptera que des valeurs.

Vous obtiendrez des zéros sur les articles qui n'ont pas été visionnés récemment et vous pouvez utiliser ce champ numviews pour le tri et le filtrage.

La requête derrière PostgreSQL sera:

SELECT
    "app_article"."id",
    "app_article"."author",
    "app_article"."published",
    COUNT(
        CASE WHEN "app_readership"."what_time" < 2015-11-18 11:04:00.000000+01:00 THEN 1
        ELSE NULL END
    ) as "numviews"
FROM "app_article" LEFT OUTER JOIN "app_readership"
    ON ("app_article"."id" = "app_readership"."which_article_id")
GROUP BY "app_article"."id", "app_article"."author", "app_article"."published"

Si nous voulons suivre uniquement les requêtes uniques, nous pouvons ajouter une distinction dans Count et faire en sorte que notre clause When renvoie la valeur que nous voulons distinguer.

from Django.db.models import Count, Case, When, CharField, F
Article.objects.annotate(
    numviews=Count(Case(
        When(readership__what_time__lt=treshold, then=F('readership__reader')), # it can be also `readership__reader_id`, it doesn't matter
        output_field=CharField(),
    ), distinct=True)
)

Cela produira:

SELECT
    "app_article"."id",
    "app_article"."author",
    "app_article"."published",
    COUNT(
        DISTINCT CASE WHEN "app_readership"."what_time" < 2015-11-18 11:04:00.000000+01:00 THEN "app_readership"."reader_id"
        ELSE NULL END
    ) as "numviews"
FROM "app_article" LEFT OUTER JOIN "app_readership"
    ON ("app_article"."id" = "app_readership"."which_article_id")
GROUP BY "app_article"."id", "app_article"."author", "app_article"."published"

Pour Django <1.8 et PostgreSQL

Vous pouvez simplement utiliser raw pour exécuter une instruction SQL créée par de nouvelles versions de Django. Apparemment, il n’existe pas de méthode simple et optimisée pour interroger ces données sans utiliser raw (même avec extra, il existe quelques problèmes d’injection de la clause requise JOIN).

Articles.objects.raw('SELECT'
    '    "app_article"."id",'
    '    "app_article"."author",'
    '    "app_article"."published",'
    '    COUNT('
    '        DISTINCT CASE WHEN "app_readership"."what_time" < 2015-11-18 11:04:00.000000+01:00 THEN "app_readership"."reader_id"'
    '        ELSE NULL END'
    '    ) as "numviews"'
    'FROM "app_article" LEFT OUTER JOIN "app_readership"'
    '    ON ("app_article"."id" = "app_readership"."which_article_id")'
    'GROUP BY "app_article"."id", "app_article"."author", "app_article"."published"')
94
GwynBleidD

Pour Django> = 2.0, vous pouvez utiliser Agrégation conditionnelle avec un argument filter. dans les fonctions d'agrégation:

from datetime import timedelta
from Django.utils import timezone
from Django.db.models import Count, Q # need import

Article.objects.annotate(
    numviews=Count(
        'readership__reader__id', 
        filter=Q(readership__what_time__gt=timezone.now() - timedelta(minutes=30)), 
        distinct=True
    )
)
22
dtatarkin