使用左外连接注释Django查询集?

时间:2011-06-27 23:21:11

标签: django

说我有一个模特:

class Foo(models.Model):
    ...

和另一个基本上为每个用户提供有关Foo的信息的模型:

class UserFoo(models.Model):
    user = models.ForeignKey(User)
    foo = models.ForeignKey(Foo)
    ...

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

我想生成Foo s的查询集,但使用基于UserFoo的(可选)相关user=request.user进行注释。

所以它实际上是LEFT OUTER JOIN on (foo.id = userfoo.foo_id AND userfoo.user_id = ...)

10 个答案:

答案 0 :(得分:16)

raw的解决方案可能看起来像

foos = Foo.objects.raw("SELECT foo.* FROM foo LEFT OUTER JOIN userfoo ON (foo.id = userfoo.foo_id AND foo.user_id = %s)", [request.user.id])

您需要修改SELECT以包含userfoo中的额外字段,这些字段将注释到查询集中生成的Foo个实例。

答案 1 :(得分:9)

  

注意:此方法在Django 1.6+中不起作用。如tcarobruce的comment below中所述,promote参数已作为ticket #19849: ORM Cleanup的一部分删除。


Django没有提供完全内置的方法来实现这一点,但是构建一个完全原始的查询并不是必需的。 (此方法无法从*中选择UserFoo,因此我使用.comment作为示例字段来包含UserFoo。)

QuerySet.extra() method允许我们在查询的SELECT和WHERE子句中添加术语。我们使用此选项在结果中包含UserFoo表格中的字段,并将UserFoo匹配限制为当前用户。

results = Foo.objects.extra(
    select={"user_comment": "UserFoo.comment"},
    where=["(UserFoo.user_id IS NULL OR UserFoo.user_id = %s)"],
    params=[request.user.id]
)

此查询仍需要UserFoo表。可以使用.extras(tables=...)来获取隐式INNER JOIN,但对于OUTER JOIN,我们需要自己修改内部查询对象。

connection = (
    UserFoo._meta.db_table, User._meta.db_table,  # JOIN these tables
    "user_id",              "id",                 # on these fields
)

results.query.join(  # modify the query
    connection,      # with this table connection
    promote=True,    # as LEFT OUTER JOIN
)

我们现在可以评估结果。每个实例都有.user_comment属性,其中包含UserFoo的值,如果不存在,则为None

print results[0].user_comment

(归功于Colin Copeland的this blog post向我展示了如何进行外部联接。)

答案 2 :(得分:9)

这个答案可能并不完全是你想要的,但是因为它是谷歌搜索" django annotate outer join"的第一个结果。所以我会在这里发布。

注意:在Djang 1.7上测试

假设您有以下型号

class User(models.Model):
    name = models.CharField()

class EarnedPoints(models.Model):
    points = models.PositiveIntegerField()
    user = models.ForgeinKey(User)

要获得总用户点数,您可以执行类似的操作

 User.objects.annotate(points=Sum("earned_points__points"))

这会有效但是会返回没有积分的用户,这里我们需要外部联接而不需要任何直接攻击或原始SQL

你可以通过这样做来实现这个目标

 users_with_points = User.objects.annotate(points=Sum("earned_points__points"))
 result = users_with_points | User.objects.exclude(pk__in=users_with_points)

这将被翻译为OUTER LEFT JOIN,并且将返回所有用户。没有积分的用户在其点属性中将具有None值。

希望有所帮助

答案 3 :(得分:3)

您建议的两个查询与您将获得的查询一样好(不使用raw()),此类查询目前在ORM中无法表示。

答案 4 :(得分:3)

我偶然发现了这个问题,如果不诉诸原始SQL我无法解决,但我不想重写整个查询。

以下是如何使用外部原始sql扩充查询集的说明,而不必关心生成查询集的实际查询。

这是一个典型的场景:你有一个类似reddit的站点,它有一个LinkPost模型和一个UserPostVote模式,如下所示:

class LinkPost(models.Model):
some fields....

class UserPostVote(models.Model):
    user = models.ForeignKey(User,related_name="post_votes")
    post = models.ForeignKey(LinkPost,related_name="user_votes")
    value = models.IntegerField(null=False, default=0)

其中userpostvote表收集用户对帖子的投票。 现在,您正在尝试为具有分页应用的用户显示首页,但您希望用户投票的帖子的箭头为红色。

首先,您会收到该页面的帖子:

post_list = LinkPost.objects.all()
paginator = Paginator(post_list,25)
posts_page = paginator.page(request.GET.get('page'))

所以现在你有一个由django paginator生成的QuerySet posts_page,它选择要显示的帖子。现在,我们如何在每个帖子上添加用户投票的注释,然后再将其呈现在模板中?

这就是它变得棘手的地方,我无法找到一个干净的ORM解决方案。 select_related将不允许您只获得与登录用户相对应的投票,并且在帖子上循环会进行一堆查询而不是一个查询,并且完成所有原始意味着我们无法使用分页应用程序中的查询集。

所以我就是这样做的:

q1 = posts_page.object_list.query  # The query object of the queryset
q1_alias = q1.get_initial_alias()  # This forces the query object to generate it's sql
(q1str, q1param) = q1.sql_with_params() #This gets the sql for the query along with 
                                        #parameters, which are none in this example

我们现在有查询集的查询,只需将它包装,别名和左外连接包含在其中:

q2_augment = "SELECT B.value as uservote, A.* 
from ("+q1str+") A LEFT OUTER JOIN reddit_userpostvote B 
ON A.id = B.post_id AND B.user_id = %s"
q2param = (request.user.id,)
posts_augmented = LinkPost.objects.raw(q2_augment,q1param+q2param)

瞧!现在我们可以访问post.uservote以获取增强查询集中的帖子。 我们只需用一个查询来访问数据库。

答案 5 :(得分:2)

您可以使用simonw的django-queryset-transform来避免对原始SQL查询进行硬编码 - 代码看起来像这样:

def userfoo_retriever(qs):
    userfoos = dict((i.pk, i) for i in UserFoo.objects.filter(foo__in=qs))
    for i in qs:
        i.userfoo = userfoos.get(i.pk, None)

for foo in Foo.objects.filter(…).tranform(userfoo_retriever):
    print foo.userfoo

这种方法非常成功,可以满足这种需求并有效地检索M2M值;您的查询计数不会那么低但是在某些数据库( cough MySQL cough )上执行两个更简单的查询通常比具有复杂JOIN的查询更快且许多我最需要它的情况有额外的复杂性,甚至更难入侵ORM表达式。

答案 6 :(得分:0)

至于外连接: 从foo获得包含对userfoo中列的引用的查询集qs后,可以将内部联接提升为外部联接 qs.query.promote_joins(["userfoo"])

答案 7 :(得分:0)

您不应该因此而诉诸extraraw

以下情况应该有效。

Foo.objects.filter(
    Q(userfoo_set__user=request.user) |
    Q(userfoo_set=None)  # This forces the use of LOUTER JOIN.
).annotate(
    comment=F('userfoo_set__comment'),
    # ... annotate all the fields you'd like to see added here.
)

答案 8 :(得分:0)

我看到不使用raw等的唯一方法就是这样:

Foo.objects.filter(
    Q(userfoo_set__isnull=True)|Q(userfoo_set__isnull=False)
).annotate(bar=Case(
    When(userfoo_set__user_id=request.user, then='userfoo_set__bar')
))

双重Q技巧可确保您获得左外部连接。

不幸的是,您无法在request.user中设置filter()条件,因为它可能会过滤掉用户名错误的UserFoo实例上的成功联接,因此会过滤出您想要保留的Foo行(因此,理想情况下,您希望在ON连接子句中而不是在WHERE子句中使用该条件。)

由于无法过滤出具有不想要的user值的行,因此必须使用CASE从UserFoo中选择行。

还请注意,一个Foo可能会连接到许多UserFoo记录,因此您可能需要考虑采用某种方法从输出中检索不同的Foos。

答案 9 :(得分:-1)

父母的评论使我走对了路:

from django.db.models.sql.datastructures import Join

for alias in qs.query.alias_map.values():
  if isinstance(alias, Join):
    alias.nullable = True

qs.query.promote_joins(qs.query.tables)