说我有一个模特:
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 = ...)
答案 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)
您不应该因此而诉诸extra
或raw
。
以下情况应该有效。
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)