Django左外连接

Rod*_*Day 23 python django orm django-models

我有一个网站,用户可以在其中查看电影列表,并为他们创建评论.

用户应该能够看到所有电影的列表.此外,如果他们审查了电影,他们应该能够看到他们给出的分数.如果没有,则仅显示没有得分的电影.

他们根本不关心其他用户提供的分数.

考虑以下 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")
Run Code Online (Sandbox Code Playgroud)

我本质上想要的是这个

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

考虑以下内容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)
Run Code Online (Sandbox Code Playgroud)

两个测试应该打印完全相同的输出,但是,只有原始版本吐出正确的结果表:

raw
---
A 1
B None
C 3

orm改为返回此

orm
---
A 1
C 3

任何尝试加入其他主题的尝试,那些没有来自用户"johnny"的评论,都会产生以下结果:

orm
---
A 1
A 4
B 5
C 3
C 6
Run Code Online (Sandbox Code Playgroud)

如何使用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')
for topic in chain(topics, noned):
    ...

编辑:这可以更好一点,但仍然很糟糕:

    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

tri*_*het 23

首先,没有办法(atm Django 1.9.7)使用Django的ORM表示您发布的原始查询,完全按照您的意愿 ; 虽然,您可以通过以下方式获得相同的预期结果:

>>> 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)]
Run Code Online (Sandbox Code Playgroud)

这里为第一个查询生成的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
Run Code Online (Sandbox Code Playgroud)

一些笔记

  • 毫不犹豫地使用原始查询,特别是当性能是最重要的事情时.而且,有时它是必须的,因为你不能使用Django的ORM获得相同的结果; 在其他情况下,你可以,但有一段时间,干净,易懂的代码比这段代码中的性能更重要.
  • distinct在这个答案中使用了位置参数,该答案仅适用于PostgreSQL,atm.在文档中,您可以看到有关条件表达式的更多信息.


Aya*_*Aya 9

我本质上想要的是这个

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

...或者,也许这个避免子查询的等价物......

select * from bar_topic
left join bar_record
on bar_record.topic_id = bar_topic.id and bar_record.user_id = 1
Run Code Online (Sandbox Code Playgroud)

我想知道如何有效地做到这一点,或者,如果不可能的话,我想知道为什么这是不可能的......

除非你使用原始查询,否则Django的ORM是不可能的,这就是原因.

QuerySetobjects(django.db.models.query.QuerySet)有一个queryattribute(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
Run Code Online (Sandbox Code Playgroud)

......由于这个原因,显然不会起作用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)}
Run Code Online (Sandbox Code Playgroud)

请注意,Django仅支持两个可能的join_types,INNER JOINLEFT OUTER JOIN.

现在,我们可以使用Query对象的promote_joins方法LEFT OUTER JOINbar_record表上使用a ...

>>> 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)}
Run Code Online (Sandbox Code Playgroud)

...将查询更改为...

>>> 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
Run Code Online (Sandbox Code Playgroud)

...但是,这仍然没有用,因为连接将始终匹配一行,即使它不属于正确的用户,并且该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
Run Code Online (Sandbox Code Playgroud)

......但最终还是遇到了同样的问题.

不幸的是,ORM产生的连接存在一个基本限制,因为它们只能是......

(LEFT OUTER|INNER) JOIN <lhs_alias> ON (<lhs_alias>.<lhs_join_col> = <rhs_alias>.<rhs_join_col>)
Run Code Online (Sandbox Code Playgroud)

...所以除了使用原始查询之外,实际上没有办法实现所需的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
Run Code Online (Sandbox Code Playgroud)

...这很简单,可以兼容所有Django支持的后端.

  • 这本质上是我找到的关于 Query 内部工作原理的唯一文档(除了代码中的注释),所以感谢您的回答 (4认同)

hyn*_*cer 8

trinchet的回答启发的这种更通用的解决方案也适用于其他数据库:

>>> qs = Topic.objects.annotate(
...         f=Max(Case(When(record__user=johnny, then=F('record__value'))))
... )
Run Code Online (Sandbox Code Playgroud)

示例数据

>>> print(qs.values_list('name', 'f'))
[(u'A', 1), (u'B', None), (u'C', 3)]
Run Code Online (Sandbox Code Playgroud)

验证查询

>>> 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
Run Code Online (Sandbox Code Playgroud)

优点(与原始解决方案相比)

  • 它也适用于SQLite.
  • 无论如何,查询集都可以轻松过滤或排序.
  • 不需要任何类型演员output_field.
  • 这些方法valuesvalues_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')
Run Code Online (Sandbox Code Playgroud)

可以通过anotate方法将更多来自Record的字段添加到具有良好助记符名称的结果中.

我同意其他作者可以对其进行优化,但可读性很重要.

编辑:如果聚合函数Max替换为相同的结果Min.Min和Max都忽略NULL值,可以在任何类型上使用,例如用于字符串.如果不保证左连接是唯一的,则聚合很有用.如果字段是数字,则Avg在左连接上使用平均值会很有用.


Tod*_*dor 7

我就是这样做的.两个查询,而不是一个:

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
Run Code Online (Sandbox Code Playgroud)

好处是你得到了整个Record对象.因此,考虑一种情况,您不仅要展示value,而且time-stamp还要展示.

仅为了记录,我想再展示一个解决方案.extra.我印象深刻,没有人提到它,因为它应该产生最好的性能.

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
Run Code Online (Sandbox Code Playgroud)

两种解决方案都可以抽象为自定义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
Run Code Online (Sandbox Code Playgroud)


e4c*_*4c5 6

原始查询。

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
            ''')
Run Code Online (Sandbox Code Playgroud)

您似乎自己知道答案。当您无法使ORM查询完全按照您希望的方式运行时,使用原始查询没有任何问题。

原始查询的一个主要缺点是它们不像ORM查询那样被缓存。这意味着,如果您对原始查询集进行两次迭代,查询将被重复。另一个是您不能在其上调用.count()。

空外键

您可以null=True通过外键中的设置来强制ORM使用LEFT OUTER JOIN 。按原样使用表执行此操作。

print Record.objects.filter(user_id=8).select_related('topic').query
Run Code Online (Sandbox Code Playgroud)

结果是

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
Run Code Online (Sandbox Code Playgroud)

现在设置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
Run Code Online (Sandbox Code Playgroud)

请注意查询是如何突然变为的LEFT OUTER JOIN。但是我们还没有走出困境,因为表格的顺序应该颠倒!因此,除非您可以重构模型,否则如果没有已经尝试过的链或UNION,那么可能无法完全实现ORM LEFT OUTER JOIN。


bet*_*ros 6

Django 2.0 引入了FilteredRelation对象,我相信这就是你想要的。这

print('\nnew orm\n---')
with self.assertNumQueries(1):
    topics = Topic.objects.annotate(
        filtered_record=FilteredRelation(
            'record', condition=Q(record__user_id=1)
        )
    ).values_list('name', 'filtered_record__value')

    for topic in topics:
        print(*topic)
Run Code Online (Sandbox Code Playgroud)

产生预期的表:

new orm
---
A 1
B None
C 3
Run Code Online (Sandbox Code Playgroud)

查询 Django 输出:

SELECT "bar_topic"."name", filtered_record."value" 
FROM "bar_topic" LEFT OUTER JOIN "bar_record" filtered_record 
ON ("bar_topic"."id" = filtered_record."topic_id" 
AND (filtered_record."user_id" = 1))
Run Code Online (Sandbox Code Playgroud)