web-dev-qa-db-ja.com

Django左外部結合

ユーザーが映画のリストを表示し、それらのレビューを作成できるWebサイトがあります。

ユーザーはすべての映画のリストを見ることができるはずです。さらに、映画をレビューした場合は、彼らが与えたスコアを見ることができるはずです。そうでない場合、映画はスコアなしで表示されます。

彼らは、他のユーザーが提供するスコアをまったく気にしません。

次のmodels.py

from Django.contrib.auth.models import User
from Django.db import models


class Topic(models.Model):
    name = models.TextField()

    def __str__(self):
        return self.name


class Record(models.Model):
    user = models.ForeignKey(User)
    topic = models.ForeignKey(Topic)
    value = models.TextField()

    class Meta:
        unique_together = ("user", "topic")

私が本質的に欲しいのはこれです

select * from bar_topic
left join (select topic_id as tid, value from bar_record where user_id = 1)
on tid = bar_topic.id

次のtest.pyコンテキスト用:

from Django.test import TestCase

from bar.models import *


from Django.db.models import Q

class TestSuite(TestCase):

    def setUp(self):
        t1 = Topic.objects.create(name="A")
        t2 = Topic.objects.create(name="B")
        t3 = Topic.objects.create(name="C")
        # 2 for Johnny
        johnny = User.objects.create(username="Johnny")
        johnny.record_set.create(topic=t1, value=1)
        johnny.record_set.create(topic=t3, value=3)
        # 3 for Mary
        mary = User.objects.create(username="Mary")
        mary.record_set.create(topic=t1, value=4)
        mary.record_set.create(topic=t2, value=5)
        mary.record_set.create(topic=t3, value=6)

    def test_raw(self):
        print('\nraw\n---')
        with self.assertNumQueries(1):
            topics = Topic.objects.raw('''
                select * from bar_topic
                left join (select topic_id as tid, value from bar_record where user_id = 1)
                on tid = bar_topic.id
                ''')
            for topic in topics:
                print(topic, topic.value)

    def test_orm(self):
        print('\norm\n---')
        with self.assertNumQueries(1):
            topics = Topic.objects.filter(Q(record__user_id=1)).values_list('name', 'record__value')
            for topic in topics:
                print(*topic)

両方のテストはまったく同じ出力を出力する必要がありますが、生のバージョンのみが正しい結果テーブルを出力します。

raw 
 --- 
 A 1 
 Bなし
 C 3

ormは代わりにこれを返します

orm 
 --- 
 A 1 
 C 3

ユーザー "johnny"からのレビューがないトピックの残りに参加しようとすると、次の結果になります。

orm
---
A 1
A 4
B 5
C 3
C 6

Django ORMを使用して生のクエリの単純な動作を実現するにはどうすればよいですか?

編集:この種の作品が非常に貧しいようだ:

topics = Topic.objects.filter(record__user_id = 1).values_list( 'name'、 'record__value')
 noned = Topic.objects.exclude(record__user_id = 1).values_list( 'name')
チェーン内のトピックの場合(トピック、未登録):
 ...

編集:これは少し良くなりますが、まだ悪いです:

    topics = Topic.objects.filter(record__user_id = 1).annotate(value = F( 'record__value'))
 topics == Topic.objects.exclude(pk__in = topics)
orm 
 --- 
 A 1 
 B 5 
 C 3
22
RodericDay

まず第一に、投稿した生のクエリの表現with DjangoのORMを持つ方法(atm Django 1.9.7)はありません正確に必要に応じて;ただし、次のようなもので同じ望ましい結果を得ることができます。

>>> Topic.objects.annotate(
        f=Case(
            When(
                record__user=johnny, 
                then=F('record__value')
            ), 
            output_field=IntegerField()
        )
    ).order_by(
        'id', 'name', 'f'
    ).distinct(
        'id', 'name'
    ).values_list(
        'name', 'f'
    )
>>> [(u'A', 1), (u'B', None), (u'C', 3)]

>>> Topic.objects.annotate(f=Case(When(record__user=may, then=F('record__value')), output_field=IntegerField())).order_by('id', 'name', 'f').distinct('id', 'name').values_list('name', 'f')
>>> [(u'A', 4), (u'B', 5), (u'C', 6)]

ここで、最初のクエリに対して生成されたSQL:

>>> print Topic.objects.annotate(f=Case(When(record__user=johnny, then=F('record__value')), output_field=IntegerField())).order_by('id', 'name', 'f').distinct('id', 'name').values_list('name', 'f').query

>>> SELECT DISTINCT ON ("payments_topic"."id", "payments_topic"."name") "payments_topic"."name", CASE WHEN "payments_record"."user_id" = 1 THEN "payments_record"."value" ELSE NULL END AS "f" FROM "payments_topic" LEFT OUTER JOIN "payments_record" ON ("payments_topic"."id" = "payments_record"."topic_id") ORDER BY "payments_topic"."id" ASC, "payments_topic"."name" ASC, "f" ASC

いくつかのメモ

  • 特にパフォーマンスが最も重要な場合は、生のクエリを使用することをためらいません。さらに、DjangoのORMを使用して同じ結果を得ることができないため、必須の場合もあります。他の場合は可能ですが、コードのこの部分でのパフォーマンスよりも、クリーンでわかりやすいコードを保持することが重要な場合があります。
  • distinctは位置引数付きでこの回答で使用されます。これは、PostgreSQLでのみ使用可能です。atm。ドキュメントでは 条件式 の詳細を見ることができます。
22
trinchet

私が本質的に欲しいのはこれです

_select * from bar_topic
left join (select topic_id as tid, value from bar_record where user_id = 1)
on tid = bar_topic.id
_

...または、おそらくサブクエリを回避するこの同等の...

_select * from bar_topic
left join bar_record
on bar_record.topic_id = bar_topic.id and bar_record.user_id = 1
_

私はこれを効果的に行う方法を知りたい、または不可能な場合は、なぜ不可能なのかの説明を知りたい...

生のクエリを使用しない限り、DjangoのORMでは不可能です。その理由は次のとおりです。

QuerySetオブジェクト(_Django.db.models.query.QuerySet_)には、実行される実際のクエリの表現であるquery属性(_Django.db.models.sql.query.Query_)があります。これらのQueryオブジェクトには___str___メソッドが役立つので、それを印刷して内容を確認できます。

簡単なQuerySet...から始めましょう。

_>>> from bar.models import *
>>> qs = Topic.objects.filter(record__user_id=1)
>>> print qs.query
SELECT "bar_topic"."id", "bar_topic"."name" FROM "bar_topic" INNER JOIN "bar_record" ON ("bar_topic"."id" = "bar_record"."topic_id") WHERE "bar_record"."user_id" = 1
_

... _INNER JOIN_により、明らかに動作しません。

Queryオブジェクトの内部を詳しく見ると、実行されるテーブル結合を決定する_alias_map_属性があります...

_>>> from pprint import pprint
>>> pprint(qs.query.alias_map)
{u'bar_record': JoinInfo(table_name=u'bar_record', rhs_alias=u'bar_record', join_type='INNER JOIN', lhs_alias=u'bar_topic', lhs_join_col=u'id', rhs_join_col='topic_id', nullable=True),
 u'bar_topic': JoinInfo(table_name=u'bar_topic', rhs_alias=u'bar_topic', join_type=None, lhs_alias=None, lhs_join_col=None, rhs_join_col=None, nullable=False),
 u'auth_user': JoinInfo(table_name=u'auth_user', rhs_alias=u'auth_user', join_type='INNER JOIN', lhs_alias=u'bar_record', lhs_join_col='user_id', rhs_join_col=u'id', nullable=False)}
_

Djangoは、可能な2つの_join_type_ s、_INNER JOIN_および_LEFT OUTER JOIN_のみをサポートすることに注意してください。

ここで、canQueryオブジェクトの_promote_joins_メソッドを使用して、_LEFT OUTER JOIN_テーブルで_bar_record_を使用します...

_>>> qs.query.promote_joins(['bar_record'])
>>> pprint(qs.query.alias_map)
{u'bar_record': JoinInfo(table_name=u'bar_record', rhs_alias=u'bar_record', join_type='LEFT OUTER JOIN', lhs_alias=u'bar_topic', lhs_join_col=u'id', rhs_join_col='topic_id', nullable=True),
 u'bar_topic': JoinInfo(table_name=u'bar_topic', rhs_alias=u'bar_topic', join_type=None, lhs_alias=None, lhs_join_col=None, rhs_join_col=None, nullable=False),
 u'auth_user': JoinInfo(table_name=u'auth_user', rhs_alias=u'auth_user', join_type='LEFT OUTER JOIN', lhs_alias=u'bar_record', lhs_join_col='user_id', rhs_join_col=u'id', nullable=False)}
_

...これはクエリを...に変更します.

_>>> print qs.query
SELECT "bar_topic"."id", "bar_topic"."name" FROM "bar_topic" LEFT OUTER JOIN "bar_record" ON ("bar_topic"."id" = "bar_record"."topic_id") WHERE "bar_record"."user_id" = 1
_

...しかし、正しいユーザーに属していなくても、結合は常に行に一致し、WHERE句がそれを除外するため、これはまだ役に立たない。

values_list()を使用すると、自動的に_join_type_...に影響します。

_>>> qs = Topic.objects.filter(record__user_id=1).values_list('name', 'record__value')
>>> print qs.query
SELECT "bar_topic"."name", "bar_record"."value" FROM "bar_topic" LEFT OUTER JOIN "bar_record" ON ("bar_topic"."id" = "bar_record"."topic_id") WHERE "bar_record"."user_id" = 1
_

...しかし、最終的には同じ問題に悩まされます。

残念ながら、ORMによって生成される結合には基本的な制限があります。というのは、それらは次の形式にしかなれないからです...

_(LEFT OUTER|INNER) JOIN <lhs_alias> ON (<lhs_alias>.<lhs_join_col> = <rhs_alias>.<rhs_join_col>)
_

...そのため、生のクエリを使用する以外に、目的のSQLを実現する方法はありません。

もちろん、annotate()extra()のようなものでハックすることもできますが、それらはおそらくパフォーマンスがはるかに低く、おそらく生のSQLよりも読みにくいクエリを生成します。


...そして提案された代替案。

個人的には、生のクエリを使用するだけです...

_select * from bar_topic
left join bar_record
on bar_record.topic_id = bar_topic.id and bar_record.user_id = 1
_

...これは、Djangoがサポートするすべてのバックエンドと互換性があるほど単純です。

9
Aya

これは私がそれをする方法です。 1つではなく2つのクエリ:

class Topic(models.Model):
    #...

    @property
    def user_value(self):
        try:
            return self.user_records[0].value
        except IndexError:
            #This topic does not have 
            #a review by the request.user
            return None
        except AttributeError:
            raise AttributeError('You forgot to prefetch the user_records')
            #or you can just
            return None

#usage
topics = Topic.objects.all().prefetch_related(
    models.Prefetch('record_set',
        queryset=Record.objects.filter(user=request.user),
        to_attr='user_records'
    )
)

for topic in topics:
    print topic.user_value

利点は、Recordオブジェクト全体を取得できることです。したがって、valueだけでなくtime-stampも表示したい状況を考えてください。

記録のために、.extraを使用したもう1つのソリューションを示します。可能な限り最高のパフォーマンスが得られるため、誰も言及していないことに感銘を受けました。

topics = Topic.objects.all().extra(
    select={
        'user_value': """SELECT value FROM myapp_record 
            WHERE myapp_record.user_id = %s
            AND myapp_record.topic_id = myapp_topic.id 
        """
    },
    select_params=(request.user.id,)
)

for topic in topics
    print topic.user_value

両方のソリューションは、再利用のためにカスタムTopicQuerySetクラスに抽象化できます。

class TopicQuerySet(models.QuerySet):

    def prefetch_user_records(self, user):
        return self.prefetch_related(
            models.Prefetch('record_set',
                queryset=Record.objects.filter(user=request.user),
                to_attr='user_records'
            )
        )

    def annotate_user_value(self, user):
        return self.extra(
            select={
                'user_value': """SELECT value FROM myapp_record 
                    WHERE myapp_record.user_id = %s
                    AND myapp_record.topic_id = myapp_topic.id 
                """
            },
            select_params=(user.id,)
        )

class Topic(models.Model):
    #...

    objects = TopicQuerySet.as_manager()


#usage
topics = Topic.objects.all().annotate_user_value(request.user)
#or
topics = Topic.objects.all().prefetch_user_records(request.user)

for topic in topics:
    print topic.user_value
7
Todor

このより普遍的なソリューショントリンシェットの答え に触発されて、他のデータベースでも動作します:

_>>> qs = Topic.objects.annotate(
...         f=Max(Case(When(record__user=johnny, then=F('record__value'))))
... )
_

サンプルデータ

_>>> print(qs.values_list('name', 'f'))
[(u'A', 1), (u'B', None), (u'C', 3)]
_

クエリを検証する

_>>> print(qs.query)  # formated and removed excessive double quotes
SELECT bar_topic.id, bar_topic.name,
       MAX(CASE WHEN bar_record.user_id = 1 THEN bar_record.value ELSE NULL END) AS f
FROM bar_topic LEFT OUTER JOIN bar_record ON (bar_topic.id = bar_record.topic_id)
GROUP BY bar_topic.id, bar_topic.name
_

利点(元のソリューションと比較して)

  • SQLiteでも機能します。
  • クエリセットは、どのように簡単にフィルタリングまたはソートできます。
  • 型キャスト_output_field_は不要です。
  • メソッドvaluesまたはvalues_list(*field_names)は、より単純な_GROUP BY_に便利ですが、必須ではありません。

左結合は、関数を記述することでさらに読みやすくすることができます。

_from Django.db.models import Max, Case, When, F

def left_join(result_field, **lookups):
    return Max(Case(When(then=F(result_field), **lookups)))

>>> Topic.objects.annotate(
...         record_value=left_join('record__value', record__user=johnny),
... ).values_list('name', 'record_value')
_

ニースのニーモニック名を使用して、この方法でanotateメソッドを使用してRecordのフィールドを追加できます。

私は他の著者に同意しますが、それは最適化できますが、 可読性カウント

[〜#〜] edit [〜#〜]:集約関数MaxMin。最小値と最大値の両方がNULL値を無視し、任意のタイプで使用できます。文字列用。集約は、左結合が一意であることが保証されていない場合に役立ちます。フィールドが数値である場合、左結合で平均値Avgを使用すると便利です。

7
hynekcer

生のクエリ。

topics = Topic.objects.raw('''
            select * from bar_topic
            left join (select topic_id as tid, value from bar_record where user_id = 1) AS subq
            on tid = bar_topic.id
            ''')

あなたは自分で答えを知っているようです。 ORMクエリを希望どおりに正確に動作させることができない場合、生のクエリを使用しても問題はありません。

生のクエリの主な欠点は、ORMクエリのようにキャッシュされないことです。つまり、生のクエリセットを2回繰り返すと、クエリが繰り返されます。もう1つは、.count()を呼び出せないことです。

ヌル外部キー

外部キーにnull=Trueを設定することにより、ORMにLEFT OUTER JOINを使用させることができます。テーブルをそのまま使用してこれを行います。

print Record.objects.filter(user_id=8).select_related('topic').query

結果は

SELECT "bar_record"."id", "bar_record"."user_id", "bar_record"."topic_id", "bar_record"."value", "bar_topic"."id", "bar_topic"."name" FROM "bar_record"
INNER JOIN "bar_topic" ON ( "bar_record"."topic_id" = "bar_topic"."id" ) WHERE "bar_record"."user_id" = 8

Null = Trueに設定し、上記と同じORMクエリを実行します。結果は

SELECT "bar_record"."id", "bar_record"."user_id", "bar_record"."topic_id", "bar_record"."value", "bar_topic"."id", "bar_topic"."name" FROM "bar_record" 
LEFT OUTER JOIN "bar_topic" ON ( "bar_record"."topic_id" = "bar_topic"."id" ) WHERE "bar_record"."user_id" = 8

クエリがLEFT OUTER JOINに突然変更されたことに注意してください。しかし、テーブルの順序を逆にする必要があるため、私たちはまだ森から出ていません!したがって、モデルを再構築できない限り、ORM LEFT OUTER JOINは、既に試してみたチェーンまたはUNIONなしでは完全に不可能な場合があります。

5
e4c5