2012-05-20 3 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> 

Есть ли конкретный способ это может быть достигнуто с помощью ORM Джанго, или я должен использовать необработанный запрос SQL? И каким бы самым эффективным способом было добиться этого в сыром SQL?

+0

Учитывая, что в вашей таблице «голосование» есть голос, как вы можете указать, к какой таблице контента он относится? Что, если 'content_id' существует в нескольких таблицах? – eggyal

+0

Прошу прощения, я забыл включить столбец. – mburke13

ответ

6

Update

Если предположить, что эти модели

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()) дает последовательность Идентификаторы User() s в порядке убывания количества голосов. Затем вы можете использовать его для получения лучших пользователей.

Вариант B. Использование User.objects.raw

Когда я использую User.objects.raw, я получил почти такой же запрос ж/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_count до User или модель профиля, например, UserProfile или другая относительная модель, как suggested by Michael Dunn. Это ведет себя намного лучше, если вы часто обращаетесь к vote_count на лету.
  • Создайте представление БД, которое делает для вас UNION, а затем сопоставьте модель с ним, это упростит конструкцию запроса.
  • Сортировка в Python, как правило, это лучший способ работы с крупномасштабными данными из-за множества наборов инструментов и способов расширения.

Вам нужны Django Модели сопоставления этих таблиц перед использованием Django ORM для запроса. Предполагая, что они являются User и Voting модели, соответствующие users и voting таблицы, вы могли бы

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 (User) и что каждая модель контента связана с голосованием в таблице голосования соотношением GenericForeignKey с идентификатором контента и содержимым Тип содержимого. Я думаю, что то, чего я хочу достичь, слишком сложно для ORM Django, поэтому я сначала пытаюсь найти лучший способ сделать это в 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. Используя raw 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; 

Альтернативой, по-видимому, является внешнее соединение таблиц контента с таблицами для голосования без шага объединения. Это может быть более результативным, но я не смог его протестировать, потому что визуальная студия продолжает переписывать мой 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

'1' в' SELECT id, 1 AS content_type_id, user_id FROM content_c' может быть опечаткой? – 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 

Если у вас уже есть данные в вашей базе данных, вам необходимо обновить количество голосов вручную в первый раз.

Смежные вопросы