2012-05-20 16 views
7

Mam zestaw tabel zawierających zawartość, która jest tworzona i głosowana przez użytkowników.Specyficzne złożone zapytanie SQL i ORM Django?

Tabela content_a

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

Tabela content_b

id 
user_id 
content 

Tabela content_c

id 
user_id 
content 

Tabela głosowanie

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 */ 

Chcę móc wybrać zestaw użytkowników i zamówić je przez sumę głosów na zawartość one produkowane. Na przykład,

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

Czy istnieje specyficzny sposób można to osiągnąć za pomocą ORM Django, czy muszę użyć kwerendy SQL surowego? A jaki byłby najskuteczniejszy sposób osiągnięcia tego w surowym SQL?

+0

Biorąc pod uwagę głosowanie w tabeli "Głosowanie", w jaki sposób można określić, do której tabeli treści się odnosi? Co się stanie, jeśli "content_id" istnieje w więcej niż jednej tabeli? – eggyal

+0

Przepraszam, zapomniałem dodać kolumnę. – mburke13

Odpowiedz

6

Aktualizacja

Zakładając, że modele są

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) 

Wariant A. Korzystanie 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') 

Powyższy ValuesQuerySet (lub użyć values_list()) daje sekwencję Identyfikatory User() s w kolejności malejącej liczby głosów. Następnie można go użyć do pobrania najlepszych użytkowników.

Wariant B. Korzystanie User.objects.raw

Gdy używam User.objects.raw, mam prawie samego zapytania 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 
)) 

Wariant C. Inne możliwe sposoby

  • De-normalizuj vote_count do User lub model profilu, na przykład UserProfile lub inny względny model, jako suggested by Michael Dunn. Zachowuje się znacznie lepiej, jeśli często korzystasz z usługi vote_count.
  • Utwórz widok bazy danych, który ma dla ciebie UNION, a następnie zmapuj do niego model, co może ułatwić tworzenie zapytania.
  • Sortowanie w Pythonie, zwykle jest to najlepszy sposób na pracę z danymi na dużą skalę, z powodu kilkunastu zestawów narzędzi i sposobów rozszerzenia.

Trzeba trochę Django modele mapowania te tabele przed użyciem ORM Django do kwerendy. Zakładając, że są User i Voting modele, które dopasowania users i voting tabele, można następnie

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

To nie zadziała, kolumna tabeli głosowania "user_id" jest związana z głosowaniem dokonanym przez użytkownika. Chcę podsumować głosy na zawartość użytkownika, a nie przez użytkownika. – mburke13

+0

@Matt Widzę. Jakie są więc modele dla 'content_a',' content_b' i 'content_c'? – okm

+0

Modele są dość ogólne. Myślę, że jedyną ważną rzeczą, na którą należy zwrócić uwagę, jest to, że każdy model treści jest powiązany z użytkownikiem przez relację ForeignKey (Użytkownik) i że każdy model zawartości jest powiązany z głosowaniem w tabeli głosowania przez relację GenericForeignKey z identyfikatorem treści i zawartością Typ zawartości. Myślę, że to, co chcę osiągnąć, jest zbyt skomplikowane dla ORM Django, więc najpierw próbuję znaleźć najlepszy sposób, aby to zrobić w SQL. Z tego powodu podałem tylko struktury tabel bazy danych zamiast modeli Django. Jeśli jest jakiś sposób, aby to zrobić w Django, chętnie bym to usłyszał. – mburke13

3

Do roztworu surowego SQL, Utworzyłem szorstką replikację problemu na ustawieniach ideone here

danych:

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

ja w zasadzie przyjąć, że content_a ma typ 1, content_b ma typ 2 i content_c ma typ 3. Używanie surowego SQL, nie wydaje się być t wo oczywiste podejścia. Pierwszym z nich jest połączenie całej zawartości, a następnie dołączenie jej do użytkowników i tabel głosowania. Przetestowałem to podejście poniżej.

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; 

Alternatywą wydaje się być zewnętrzne dołączenie tabel treści do tabel głosowania, bez kroku związkowego. To może być bardziej wydajne, ale nie byłem w stanie go przetestować, ponieważ studio graficzne ciągle przepisuje mój sql dla mnie ... Spodziewam się, że SQL będzie wyglądał mniej więcej tak (ale nie przetestowałem tego):

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

'1' w' SELECT id, 1 AS content_type_id, user_id FROM content_c' może być literówką? – okm

+0

@okm: Dzięki, że masz rację, to powinno być 3, zaktualizowałem to. – forsvarir

0

Zrobiłbym to, używając wstępnie obliczonych wartości. Najpierw zrobić osobną tabelę do przechowywania głosy, że każdy użytkownik otrzymał:

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

następnie użyć post_save signal zaktualizować hrabiego za każdym razem, głosowanie przeprowadza się:

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) 

Zastosowanie:

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

Jeśli masz już dane w bazie danych, musisz zaktualizować liczenie głosów ręcznie po raz pierwszy oczywiście.