2011-06-27 25 views
25

說我有一個模型:註解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個查詢集,但與註解基於user=request.user(可選)相關UserFoo

因此,這實際上是一個LEFT OUTER JOIN on (foo.id = userfoo.foo_id AND userfoo.user_id = ...)

+2

在可能性是兩個查詢:'UserFoo.objects.filter .select_related(用戶= request.user)( 「富」)'然後'Foo.objects.exclude(userfoo__user = request.user)'但尋找其他可能性 –

+0

什麼是最終目標/什麼是用例? –

+0

@Dan我希望這個問題很清楚:UserFoo包含關於Foo的每個用戶的信息,我想顯示一個Foo的註釋列表,其中包含來自UserFoo的用戶信息,用於request.user –

回答

17

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

以及如何選擇你想要的UserFoo字段? –

+0

以及如何避免列名衝突? –

+1

我特別選擇不從'userfoo'中選擇任何內容來防止'id'字段明顯的名稱衝突。如果你需要'userfoo'中的一個字段,你可以將查詢修改爲'SELECT foo。*,userfoo.columnA FROM foo LEFT OUTER JOIN userfoo ON(foo.id = userfoo.foo_id AND foo.user_id =%s)'和以「foos [0] .columnA'訪問 –

3

你的建議是,你會得到好這兩個查詢(不使用生()),這種類型的查詢是不是在目前的ORM所能表述。

+1

使用raw的正確答案是可以接受的;除了「使用原始」之外,還有更多。 –

+0

你可以使用ORM在Django中進行外連接嗎? –

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值;您的查詢次數不會是相當低,但在某些數據庫(咳嗽 MySQL的咳嗽)做兩個簡單的查詢,往往比一個複雜的連接和衆多的地方,我最需要的情況下,有更多的快更難以侵入ORM表達式的複雜性。

+1

作爲保持迭代器模式不變的替代方法,我創建了一個名爲https://github.com/vdboor/django-queryset-decorator的分支,這在某些邊界情況下也可能有用 – vdboor

+1

vdboor :我喜歡django-queryset-transform的全部功能,但是對於那些沒有額外的靈活性的簡單案例來說,這似乎是一個很好的節省時間的工具。 –

9

說明:此方法在Django 1.6+中不起作用。正如tcarobruce的comment below解釋的那樣,promote參數被移除的ticket #19849: ORM Cleanup一部分。


Django不提供完全內置的方式做到這一點,但它不是neccessary構建一個完全原始查詢。 (此方法不適用於從UserFoo選擇*工作,所以我使用.comment作爲一個例子場從UserFoo包括。)

QuerySet.extra() method使我們能夠項添加到SELECT和WHERE我們的查詢的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屬性都包含UserFooNone(如果不存在)的值。

print results[0].user_comment 

(感謝this blog post科林·科普蘭用於顯示我怎麼做外連接。)

+1

我得到'join()有一個意想不到的關鍵字參數'promote''。在最近的Django版本中刪除了這個功能嗎? –

+2

作爲[ORM清理](https://code.djangoproject.com/ticket/19849)的一部分,'promote'關鍵字被刪除。在Django 1.6中不再可用。 – tcarobruce

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的分頁程序生成的查詢集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

這個答案可能不是你正在尋找的,但是因爲它在google搜索「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

希望幫助

+0

這應該是迄今爲止被接受的答案!你應該得到額外的分數!這就像在乾草堆裏找到一根針!非常感謝! – Bobort

0

對於外連接: 一旦你有一個從Foo一個QuerySet qs,其中包括來自userfoo到列的引用,可以促進內連接到外部與連接 qs.query.promote_joins(["userfoo"])

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. 
)