2012-05-20 30 views
7

我有一組表格,其中包含由用戶創建並投票的內容。特定的複雜SQL查詢和Django ORM?

content_a

id   /* the id of the content */ 
user_id /* the user that contributed the content */ 
content /* the content */ 

content_b

id 
user_id 
content 

content_c

id 
user_id 
content 

投票

user_id   /* the user that made the vote */ 
content_id  /* the content the vote was made on */ 
content_type_id /* the content type the vote was made on */ 
vote   /* the value of the vote, either +1 or -1 */ 

我希望能夠通過對他們產生的內容的票數之和來選擇一組用戶和訂單他們。例如,

SELECT * FROM users ORDER BY <sum of votes on all content associated with user> 

有沒有這個可以用Django的ORM實現,或者我必須使用原始的SQL查詢具體的方法是什麼?在原始SQL中實現這一點最有效的方法是什麼?

+0

如果您在「投票」表中進行投票,您如何知道它與哪個內容表相關?如果'content_id'存在於多個表中呢? – eggyal

+0

我很抱歉,我忘了包括一列。 – mburke13

回答

6

更新

假設模型

from django.contrib.contenttypes import generic 
from django.contrib.contenttypes.models import ContentType 


class ContentA(models.Model): 
    user = models.ForeignKey(User) 
    content = models.TextField() 

class ContentB(models.Model): 
    user = models.ForeignKey(User) 
    content = models.TextField() 

class ContentC(models.Model): 
    user = models.ForeignKey(User) 
    content = models.TextField() 

class GenericVote(models.Model): 
    content_type = models.ForeignKey(ContentType) 
    object_id = models.PositiveIntegerField() 
    content_object = generic.GenericForeignKey() 
    user = models.ForeignKey(User) 
    vote = models.IntegerField(default=1) 

選項A.使用GenericVote

GenericVote.objects.extra(select={'uid':""" 
CASE 
WHEN content_type_id = {ct_a} THEN (SELECT user_id FROM {ContentA._meta.db_table} WHERE id = object_id) 
WHEN content_type_id = {ct_b} THEN (SELECT user_id FROM {ContentB._meta.db_table} WHERE id = object_id) 
WHEN content_type_id = {ct_c} THEN (SELECT user_id FROM {ContentC._meta.db_table} WHERE id = object_id) 
END""".format(
ct_a=ContentType.objects.get_for_model(ContentA).pk, 
ct_b=ContentType.objects.get_for_model(ContentB).pk, 
ct_c=ContentType.objects.get_for_model(ContentC).pk, 
ContentA=ContentA, 
ContentB=ContentB, 
ContentC=ContentC 
)}).values('uid').annotate(vc=models.Sum('vote')).order_by('-vc') 

以上ValuesQuerySet,(或使用values_list())爲您提供的序列ID爲User() s按遞減的票數順序排列。然後,您可以使用它來獲取頂級用戶。

選項B.使用User.objects.raw

當我使用User.objects.raw,我得到了幾乎相同的查詢W/the answer given by forsvarir

User.objects.raw(""" 
SELECT "{user_tbl}".*, SUM("gv"."vc") as vote_count from {user_tbl}, 
    (SELECT id, user_id, {ct_a} AS ct FROM {ContentA._meta.db_table} UNION 
    SELECT id, user_id, {ct_b} AS ct FROM {ContentB._meta.db_table} UNION 
    SELECT id, user_id, {ct_c} as ct FROM {ContentC._meta.db_table} 
    ) as c, 
    (SELECT content_type_id, object_id, SUM("vote") as vc FROM {GenericVote._meta.db_table} GROUP BY content_type_id, object_id) as gv 
WHERE {user_tbl}.id = c.user_id 
    AND gv.content_type_id = c.ct 
    AND gv.object_id = c.id 
GROUP BY {user_tbl}.id 
ORDER BY "vc" DESC""".format(
    user_tbl=User._meta.db_table, ContentA=ContentA, ContentB=ContentB, 
    ContentC=ContentC, GenericVote=GenericVote, 
    ct_a=ContentType.objects.get_for_model(ContentA).pk, 
    ct_b=ContentType.objects.get_for_model(ContentB).pk, 
    ct_c=ContentType.objects.get_for_model(ContentC).pk 
)) 

選項C.其他可能的方式

  • 解除標準化vote_countUser或配置文件模型,例如UserProfile或其他相關模型,如suggested by Michael Dunn。如果您經常訪問vote_count,這會表現得更好。
  • 建立一個DB視圖,爲您做UNION,然後將模型映射到它,這可以使查詢的構造更容易。
  • 因爲有十幾種工具包和擴展方式,所以通常它是用於處理大規模數據的最佳方式。

你需要一些Django模型映射之前使用Django的ORM這些表進行查詢。假設他們是UserVoting模型匹配usersvoting表,然後你可以

User.objects.annotate(v=models.Sum('voting__vote')).order_by('v') 
+0

這不起作用,投票表格列'user_id'與用戶進行的投票相關聯。我想對用戶的內容進行總結,而不是由用戶製作。 – mburke13

+0

@Matt我明白了。那麼'content_a','content_b'和'content_c'的模型是什麼? – okm

+0

這些模型非常通用。我認爲唯一需要注意的重要事項是每個內容模型都通過ForeignKey(用戶)關係與用戶相關聯,並且每個內容模型都與投票表中的投票有關,其中GenericForeignKey關係與內容的id和內容的內容類型。我認爲我想要實現的對於Django的ORM來說太複雜了,所以我首先試圖找出在SQL中實現它的最佳方法。正因爲如此,我只給了數據庫表結構而不是Django模型。如果在Django有辦法做到這一點,我很樂意聽到它。 – mburke13

3

對於原始SQL的解決方案,我創建你的問題的粗糙複製上ideone here

數據設置:

create table content_a(id int, user_id int, content varchar(20)); 
create table content_b(id int, user_id int, content varchar(20)); 
create table content_c(id int, user_id int, content varchar(20)); 
create table voting(user_id int, content_id int, content_type_id int, vote int); 
create table users(id int, name varchar(20)); 
insert into content_a values(1,1,'aaaa'); 
insert into content_a values(2,1,'bbbb'); 
insert into content_a values(3,1,'cccc'); 
insert into content_b values(1,2,'dddd'); 
insert into content_b values(2,2,'eeee'); 
insert into content_b values(3,2,'ffff'); 
insert into content_c values(1,1,'gggg'); 
insert into content_c values(2,2,'hhhh'); 
insert into content_c values(3,3,'iiii'); 
insert into users values(1, 'first'); 
insert into users values(2, 'second'); 
insert into users values(3, 'third'); 
insert into users values(4, 'voteonly'); 

-- user 1 net votes (2) 
insert into voting values (1, 1, 1, 1); 
insert into voting values (2, 3, 1, -1); 
insert into voting values (3, 1, 1, 1); 
insert into voting values (4, 2, 1, 1); 

-- user 2 net votes (3) 
insert into voting values (1, 2, 2, 1); 
insert into voting values (1, 1, 2, 1); 
insert into voting values (2, 3, 2, -1); 
insert into voting values (4, 2, 2, 1); 
insert into voting values (4, 2, 3, 1); 

-- user 3 net votes (-1) 
insert into voting values (2, 3, 3, -1); 

我基本上認爲content_a的類型是1,content_b的類型是2,content_c的類型是3.使用原始SQL,似乎有t我明顯的方法。首先是將所有內容組合在一起,然後將其與用戶和投票表結合起來。我已經在下面測試了這種方法。

select users.*, sum(voting.vote) 
from users, 
    voting, (
     SELECT  id, 1 AS content_type_id, user_id 
     FROM   content_a 
     UNION 
     SELECT  id, 2 AS content_type_id, user_id 
     FROM   content_b 
     UNION 
     SELECT  id, 3 AS content_type_id, user_id 
     FROM   content_c) contents 
where contents.user_id = users.id 
and voting.content_id = contents.id 
and voting.content_type_id = contents.content_type_id 
group by users.id 
order by sum(voting.vote) desc; 

替代似乎是外連接的內容表的投票表格,沒有工會的一步。這可能是更高性能的,但我一直無法測試它,因爲visual studio一直在爲我重寫我的sql ...我期望SQL看起來像這樣(但我沒有測試過):

select users.*, sum(voting.vote) 
from users, voting, content_a, content_b, content_c 
where users.id = content_a.user_id (+) 
and users.id = content_b.user_id (+) 
and users.id = content_c.user_id (+) 
and ((content_a.id = voting.content_id and voting.content_type_id = 1) OR 
    (content_b.id = voting.content_id and voting.content_type_id = 2) OR 
    (content_c.id = voting.content_id and voting.content_type_id = 3)) 
group by users.id 
order by sum(voting.vote) desc; 
+0

'SELECT id,1 as content_type_id,user_id FROM content_c'中的'1'可能是拼寫錯誤? – okm

+0

@okm:謝謝你說得對,應該是3,我已經更新了。 – forsvarir

0

我會這樣做使用預先計算的值。首先製作一個單獨的表來存儲每個用戶收到的票數:

class VotesReceived(models.Model): 
    user = models.OneToOneField(User, primary_key=True) 
    count = models.IntegerField(default=0, editable=False) 

然後用post_save signal每票由時間來更新計數:

def update_votes_received(sender, instance, **kwargs): 
    # `instance` is a Voting object 
    # assuming here that `instance.content.user` is the creator of the content 
    vr, _ = VotesReceived.objects.get_or_create(user=instance.content.user) 
    # you should recount the votes here rather than just incrementing the count 
    vr.count += 1 
    vr.save() 

models.signals.post_save.connect(update_votes_received, sender=Voting) 

用法:

user = User.objects.get(id=1) 
print user.votesreceived.count 

如果您的數據庫中已有數據,則必須在第一次手動更新投票計數。