2012-05-20 11 views
7

Tengo un conjunto de tablas que contienen contenido creado y votado por los usuarios.¿Consultas SQL complejas específicas y ORM de Django?

Tabla content_a

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

Tabla content_b

id 
user_id 
content 

Tabla content_c

id 
user_id 
content 

Tabla votación

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

Quiero ser capaz de seleccionar un conjunto de usuarios y para ellos por la suma de los votos de los contenidos que han producido. Por ejemplo,

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

¿Hay una manera específica se puede lograr usando ORM de Django, o tengo que utilizar una consulta SQL en bruto? ¿Y cuál sería la forma más eficiente de lograr esto en SQL sin procesar?

+0

Dado un voto en su tabla de "votación", ¿cómo puede saber a qué tabla de contenido se refiere? ¿Qué sucede si el 'content_id' existe en más de una tabla? – eggyal

+0

Pido disculpas, olvidé incluir una columna. – mburke13

Respuesta

6

actualización

Suponiendo que los modelos son

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) 

Opción A. Utilizando 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') 

Lo anterior ValuesQuerySet, (o utilizar values_list()) le da una secuencia de ID de User() s en el orden de recuento de votos descendentes. Podrías usarlo para buscar a los mejores usuarios.

Opción B. Usando User.objects.raw

Cuando uso User.objects.raw, tengo casi la misma consulta 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 
)) 

Opción C. Otras formas posibles

  • Desnormalizar vote_count a User o modelo de perfil, por ejemplo, UserProfile, u otro modelo relativo, como suggested by Michael Dunn. Esto se comporta mucho mejor si accede con frecuencia al vote_count.
  • Cree una vista de BD que haga el UNION s, luego asigne un modelo a ella, esto podría facilitar la construcción de la consulta.
  • Clasificar en Python, por lo general es la mejor manera de trabajar para datos a gran escala, debido a una docena de kits de herramientas y formas de extensión.

usted necesita algunos modelos Django mapeo esas tablas antes de su uso ORM de Django a consulta. Suponiendo que son User y Voting modelos que emparejan users y voting tablas, se podrían entonces

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

Esto no funcionará, la columna de la tabla de votación 'user_id' está asociada con la votación hecha por un usuario. Quiero sumar los votos sobre el contenido de un usuario, no realizado por el usuario. – mburke13

+0

@Matt ya veo. ¿Qué modelos son 'content_a',' content_b' y 'content_c'? – okm

+0

Los modelos son bastante genéricos. Creo que las únicas cosas importantes a tener en cuenta son que cada modelo de contenido está relacionado con un usuario por una relación ForeignKey (Usuario) y que cada modelo de contenido está relacionado con un voto en la mesa de votación por una relación GenericForeignKey con el id del contenido y el contenido tipo de contenido. Creo que lo que quiero lograr es demasiado complejo para el ORM de Django, así que primero intento descubrir la mejor manera de hacerlo en SQL. Por eso, solo di estructuras de tabla de base de datos en lugar de modelos de Django. Si hay una manera de hacerlo en Django, me gustaría escucharlo. – mburke13

3

Para una solución de SQL prima, he creado una réplica aproximada de su problema en la configuración ideone here

datos:

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

Básicamente, he asumido que content_a tiene un tipo de 1, content_b tiene un tipo de 2 y content_c tiene un tipo de 3. Con el SQL sin formato, parece que hay t wo enfoques obvios. El primero es unir todo el contenido, luego unirlo con los usuarios y las mesas de votación. He probado este enfoque a continuación.

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; 

La alternativa parece ser unir externamente las tablas de contenido a las tablas de votación, sin el paso de unión. Esto puede ser más eficaz, pero no he podido probarlo porque Visual Studio sigue reescribiendo mi sql para mí ... Esperaría que el SQL se vea algo así (pero no lo he probado):

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

el '1' en' SELECT id, 1 AS content_type_id, user_id FROM content_c' podría ser un error tipográfico? – okm

+0

@okm: Gracias, tienes razón, debería haber sido un 3, lo he actualizado. – forsvarir

0

Lo haría utilizando valores precalculados. En primer lugar hacer una tabla separada para almacenar los votos que cada usuario ha recibido:

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

luego usar un post_save signal para actualizar la cuenta cada vez que se hace una votación:

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) 

Uso:

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

Si ya tiene datos en su base de datos, deberá actualizar los conteos de votos manualmente la primera vez, por supuesto.

Cuestiones relacionadas