web-dev-qa-db-ja.com

WHERE句の一部としてサブクエリを使用してDjangoクエリを作成するにはどうすればよいですか?

DjangoおよびPython 3.7を使用しています。Django queryの記述方法を理解するのに問題があります。 where句の一部としてサブクエリがあります。ここにモデルがあります...

class Article(models.Model):
    objects = ArticleManager()
    title = models.TextField(default='', null=False)
    created_on = models.DateTimeField(auto_now_add=True)


class ArticleStat(models.Model):
    objects = ArticleStatManager()
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='articlestats')
    elapsed_time_in_seconds = models.IntegerField(default=0, null=False)
    votes = models.FloatField(default=0, null=False)


class StatByHour(models.Model):
    index = models.FloatField(default=0)
    # this tracks the hour when the article came out
    hour_of_day = IntegerField(
        null=False,
        validators=[
            MaxValueValidator(23),
            MinValueValidator(0)
        ]
    )

PostGresでは、クエリは次のようになります。

SELECT *
FROM article a,
     articlestat ast
WHERE a.id = ast.article_id
  AND ast.votes > 100 * (
    SELECT "index" 
    FROM statbyhour 
    WHERE hour_of_day = extract(hour from (a.created_on + 1000 * interval '1 second')))

WHERE句の一部としてサブクエリに注意してください

ast.votes > 100 * (select index from statbyhour where hour_of_day = extract(hour from (a.created_on + 1000 * interval '1 second'))) 

だから私はこのようなことができると思いました...

hour_filter = Func(
    Func(
        (F("article__created_on") + avg_fp_time_in_seconds * "interval '1 second'"),
        function='HOUR FROM'),
    function='EXTRACT')
...
votes_criterion2 = Q(votes__gte=F("article__website__stats__total_score") / F(
    "article__website__stats__num_articles") * settings.TRENDING_PCT_FLOOR *
                                StatByHour.objects.get(hour_of_day=hour_filter) * day_of_week_index)
qset = ArticleStat.objects.filter(votes_criterion1 & votes_criterion2,
                                  comments__lte=25)

ただし、これにより「キーワード 'article'をフィールドに解決できません。選択肢は、hour_of_day、id、index、num_articles、total_score」エラーになります。これは、Djangoが「StatByHour.objects」クエリを実行する前にクエリを実行しているためだと思いますが、サブクエリを実行するように書き直す方法がわかりません。同時に。

編集: K、私のサブクエリを実際の「サブクエリ」関数に移動し、OuterRefを使用して作成したフィルターを参照しました...

hour_filter = Func(
    Func(
        (F("article__created_on") + avg_fp_time_in_seconds * "interval '1 second'"),
        function='HOUR FROM'),
    function='EXTRACT')
query = StatByHour.objects.get(hour_of_day=OuterRef(hour_filter))


...
votes_criterion2 = Q(votes__gte=F("article__website__stats__total_score") / F(
    "article__website__stats__num_articles") * settings.TRENDING_PCT_FLOOR *
                                Subquery(query) * 
                 day_of_week_index)
qset = ArticleStat.objects.filter(votes_criterion1 & votes_criterion2,
                                  comments__lte=25)

そしてこれは

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

サブクエリで使用しているため、これは奇妙です。

#2を編集: 与えられた答えごとにクエリを変更した後でも...

hour_filter = Func(
    Func(
        (F("article__created_on") + avg_fp_time_in_seconds * "interval '1 second'"),
        function='HOUR FROM'),
    function='EXTRACT')
query = StatByHour.objects.filter(hour_of_day=OuterRef(hour_filter))[:1]

...
votes_criterion2 = Q(votes__gte=F("article__website__stats__total_score") / F(
    "article__website__stats__num_articles") * settings.TRENDING_PCT_FLOOR *
                                Subquery(query) *
                                day_of_week_index)
qset = ArticleStat.objects.filter(et_criterion1 & et_criterion2 & et_criterion3,
                                  votes_criterion1 & votes_criterion2,
                                  article__front_page_first_appeared_date__isnull=True,
                                  comments__lte=25)

それでもエラーが発生する

'Func' object has no attribute 'split'
6
Dave

サブクエリ 外部クエリが実行されるまで評価を延期できるように、すぐに評価されないクエリである必要があります。 get()はすぐに実行され、Querysetではなくオブジェクトインスタンスを返すため、請求書に適合しません。

ただし、filtergetに置き換えてから、_[:1]_スライスを取得すると機能します。

_StatByHour.objects.filter(hour_of_day=OuterRef('hour_filter')).values('hour_of_day')[:1]
_

OuterRef のフィールド参照が変数ではなく文字列リテラルであることを確認してください。

さらに、サブクエリは単一の列と単一の行を返す必要があります(単一のフィールドに割り当てられているため)。したがって、values()と上記のスライスを返します。

また、Qオブジェクトでサブクエリをまだ使用していません。それがうまくいくかどうかはわかりません。最初にサブクエリの出力を注釈に保存してから、それをフィルターの計算に使用する必要がある場合があります。

1
Endre Both

何が起こっているのかを明確にするために、可能な限り注釈に移動することは役に立ちます。

Extract 関数を使用して時間を取得できます。より複雑なavg_fp_time_in_secondsのものを組み込む場合は、独自のFuncを定義する必要があります。これは、独自の投稿に値するため、複製しようとしませんでした(これは、 'Func' object has no attribute 'split'エラーが発生した)。

# First, add a field for the hour 
articles_with_hour = Article.objects.annotate(created_on_hour=ExtractHour('created_on'))

# Set up the subquery, referencing the annotated field
for_this_hour = StatByHour.objects.filter(hour_of_day=OuterRef('created_on_hour'))

# Add the subquery, making sure to slice down to one value
articles_with_hour_index = articles_with_hour.annotate(
    index_for_this_hour=Subquery(for_this_hour.values('index')[:1]),
)

# Add the website averages for later calculations 
#  (note if total_score and num_articles are different field types
#  you may need an ExpressionWrapper)
articles_with_avg_website_score = articles_with_hour_index.annotate(
    average_article_score_for_website=(
        F("website__stats__total_score") / F("website__stats__num_articles")
    )
)

# Use the averages to calculate the trending floor for each article
articles_with_trending_floor = articles_with_avg_website_score.annotate(
    trending_floor=F('average_article_score_for_website') * settings.TRENDING_PCT_FLOOR,
)

# Set up the criteria, referencing fields that are already annotated on the qs
# ...
votes_gte_trending_floor_for_this_hour_criterion = Q(articlestats__votes__gte=(
    F('trending_floor')
    * F('index_for_this_hour')
    * day_of_week_index  # not sure where this comes from?
))
# ...

# Then just filter down (note this is an Article QuerySet, not ArticleStat)
qset = articles_with_trending_floor.filter(
    votes_gte_trending_floor_for_this_hour_criterion,
    # other criteria
    front_page_first_appeared_date__isnull=True,
    articlestats__comments__lte=25,
)

これらの計算の多くは簡略化でき、複数のkwargを使用して単一のannotate呼び出しですべてを実行することも可能かもしれませんが、それをすべてレイアウトすると理解が容易になると思います。

0
Fush

それ自体がhour_of_day=ExtractHour(OuterRef('article__created_on') + timedelta(seconds=avg_fp_time_in_seconds))でフィルタリングされるサブクエリによるフィルタリングを使用します。実際のコードは追加のExpressionWrapperを1つ必要とし、Django >= 2.1.0でのみ機能します。

import datetime

from Django.db import models
from Django.db.models import F, OuterRef, Subquery, Value
from Django.db.models.functions import ExtractHour, Coalesce
from Django.db.models.expressions import ExpressionWrapper


relevant_hour_stats = (
    StatByHour.objects
    .filter(
        hour_of_day=ExtractHour(ExpressionWrapper(
            OuterRef('article__created_on')  # NOTE: `OuterRef()+Expression` works only on Django >= 2.1.0
            +
            datetime.timedelta(seconds=avg_fp_time_in_seconds),
            output_field=models.DateTimeField()
        )),
    )
    .annotate(
        votes_threshold=Coalesce(
            100.0 * F('index'),
            0.0,
            output_field=models.FloatField(),
        ),
    )
    .order_by('-votes_threshold')
    # NOTE: your StatByHour model does not have unique=True on hour_of_day
    # field, so there may be several stat for same hour.
    # And from your SQL example it's unclear how should they be handled. So I
    # assume that "greatest" threshold is needed.
)

article_stats = (
    ArticleStat.objects
    .all()
    .filter(
        votes__gt=Coalesce(
            Subquery(relevant_hour_stats.values('votes_threshold')[:1]),
            Value(0.0),
            output_field=models.FloatField(),
        ),
    )
)

追伸githubに「デモプロジェクト」をセットアップして、誰でもそれを複製してローカルでアイデアを確認できるようにすると、はるかに簡単になります。

P.P.S.このコードは動作するようにテストされていますが、異なるモデル/フィールドで:

In [15]: relevant_something = (ModelOne.objects.filter(index=ExtractHour(ExpressionWrapper(OuterRef('due_date') + datetime.timedelta(seconds=1000), output_field=models.DateTimeField()))).annotate(votes_threshold=100*F('indent')).order_by('-votes_threshold'))

In [16]: ts = ModelTwo.objects.all().filter(votes__gt=Subquery(relevant_notes.values('votes_threshold')[:1], output_field=models.IntegerField()))

In [17]: print(ts.query)
SELECT 
    ...
FROM 
    "some_app_model_two" 
WHERE 
    "some_app_model_two"."votes" > (
        SELECT 
            (100 * U0."indent") AS "votes_threshold" 
        FROM 
            "some_app_model_one" U0 
        WHERE 
            U0."index" = (
                EXTRACT(
                    'hour' 
                    FROM ("some_app_model_two"."due_date" + 0:16:40) 
                    AT TIME ZONE 'America/Los_Angeles'
                )
            ) 
        ORDER BY "votes_threshold" DESC 
        LIMIT 1
    )
ORDER BY 
    "some_app_model_two"."due_date" ASC, 
    "some_app_model_two"."priority" ASC, 
    "some_app_model_two"."updated_at" DESC

それでエラーが発生した場合は、実行している実際のコードを表示してください

0
imposeren

Djangoクエリ を見てください。 SQLベースのクエリをDjangoが提供するものに変更することで問題を解決できると思います。

機能しない場合は、 未加工のSQLクエリを実行 できます。

0
Mostafa Zare

確かに Subquery ソリューションのようです。

Django> = 1.11

警告として、コードをテストしましたが、モデルだけではデータがなかったので、この答えは正しい方向にあなたを指摘するための単なる努力です

_# Query that references an outer field from another model, in this case created_on.
# On wich we are performing a lookup in order to "extract" the hour (assuming here)
# a DateTimeField or a TimeField.
stat_by_hour = StatByHour.objects.filter(hour_of_day=OuterRef('created_on__hour'))


# Then filter articles, that have articlestats.votes 
# greater than 100 * stat_by_hour.index
result = Article.objects.filter(
    articlestats__votes__gt=100 * Subquery(stat_by_hour.values('index')[:1], output_field=FloatField())
)
_

一見すると、サブクエリでorder_by('index')またはorder_by('-index')を実行する必要があるように見えます。そのため、スライス_[:1]_は最小値または最大値(あなたの要望。)

これ(または非常によく似たもの)を使用して、目的を達成できると確信しています。

0
Raydel Miranda