web-dev-qa-db-ja.com

Django querysetの条件でCountに注釈を付ける方法

Django ORMを使用すると、queryset.objects.annotate(Count('queryset_objects', gte=VALUE))のようなことができます。ドリフトをキャッチできますか?


考えられる答えを説明するために使用する簡単な例を次に示します。

Djangoウェブサイトでは、コンテンツ作成者が記事を送信し、通常のユーザーがその記事を表示(閲覧)します。記事は公開(すべての人が閲覧可能)またはドラフトモードのいずれかです。これらの要件を表すモデルは次のとおりです。

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)

私の質問は:過去30分間のユニークな読者によってソートされたすべての公開された記事を取得するにはどうすればよいですか?つまり過去30分間に発行された各記事が取得した個別の(一意の)ビューの数をカウントし、これらの個別のビューでソートされた記事のリストを作成します。


私は試した:

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

これによりエラーが発生しました:「01」またはその近くの構文エラー(「01」はextra内の日時オブジェクトでした)。続けるのはたいしたことではありません。

47
Hassan Baig

Django> = 1.8

条件付き集計 を使用:

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

説明:記事を介した通常のクエリには、numviewsフィールドで注釈が付けられます。このフィールドは、CountでラップされたCASE/WHEN式として構築され、読者の一致条件に対して1を返し、一致しない条件の読者に対してNULLを返します。カウントはヌルを無視し、値のみをカウントします。

最近表示されていない記事ではゼロが表示され、そのnumviewsフィールドを使用して並べ替えとフィルタリングを行うことができます。

PostgreSQLの背後にあるクエリは次のようになります。

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"

一意のクエリのみを追跡する場合は、Countに区別を追加し、When句で値を返すようにできます。区別する必要があります。

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

生成されるもの:

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"

Django <1.8およびPostgreSQL

Djangoの新しいバージョンで作成されたSQLステートメントの実行にrawを使用できます。どうやらrawを使用せずにそのデータをクエリするための簡単で最適化された方法はありません(extraを使用しても、必要な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

Django> = 2.0の場合、集約関数で filter引数付きの条件付き集約 を使用できます。

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