ユーザーが映画のリストを表示し、それらのレビューを作成できる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
まず第一に、投稿した生のクエリの表現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
distinct
は位置引数付きでこの回答で使用されます。これは、PostgreSQLでのみ使用可能です。atm。ドキュメントでは 条件式 の詳細を見ることができます。私が本質的に欲しいのはこれです
_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がサポートするすべてのバックエンドと互換性があるほど単純です。
これは私がそれをする方法です。 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
このより普遍的なソリューショントリンシェットの答え に触発されて、他のデータベースでも動作します:
_>>> 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
_
利点(元のソリューションと比較して)
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 [〜#〜]:集約関数Max
がMin
。最小値と最大値の両方がNULL値を無視し、任意のタイプで使用できます。文字列用。集約は、左結合が一意であることが保証されていない場合に役立ちます。フィールドが数値である場合、左結合で平均値Avg
を使用すると便利です。
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なしでは完全に不可能な場合があります。