具体复杂的 SQL 查询和 Django ORM?
Posted
技术标签:
【中文标题】具体复杂的 SQL 查询和 Django ORM?【英文标题】:Specific complex SQL query and Django ORM? 【发布时间】:2012-05-20 14:59:43 【问题描述】:我有一组表格,其中包含由用户创建和投票的内容。
表格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 中实现这一目标的最有效方法是什么?
【问题讨论】:
在您的voting
表中进行投票,您如何判断它与哪个内容表相关?如果content_id
存在于多个表中怎么办?
抱歉,我忘记添加专栏了。
【参考方案1】:
更新
假设模型是
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的ID序列,按票数降序排列。然后,您可以使用它来获取***用户。
选项 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
s 的数据库视图,然后将模型映射到它,这可以使查询的构建更容易。
在 Python 中排序,通常它是处理大规模数据的最佳方式,因为有十几种工具包和扩展方式。
在使用 Django ORM 查询之前,您需要一些 Django 模型来映射这些表。假设它们是匹配 users
和 voting
表的 User
和 Voting
模型,那么您可以
User.objects.annotate(v=models.Sum('voting__vote')).order_by('v')
【讨论】:
这不起作用,投票表列 'user_id' 与用户的投票相关联。我想对用户内容的投票进行汇总,而不是由用户进行。 @Matt 我明白了。那么content_a
、content_b
和content_c
的型号是什么?
模型非常通用。我认为唯一需要注意的重要事情是,每个内容模型都通过 ForeignKey(User) 关系与用户相关,并且每个内容模型都通过 GenericForeignKey 关系与内容的 id 和内容的 GenericForeignKey 关系与投票表中的投票相关联内容类型。我认为我想要实现的对于 Django 的 ORM 来说太复杂了,所以我首先尝试找出在 SQL 中实现它的最佳方法。因此,我只给出了数据库表结构而不是 Django 模型。如果有办法在 Django 中做到这一点,我会很高兴听到它。【参考方案2】:
对于原始 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,似乎有两种明显的方法。首先是将所有内容联合在一起,然后将其与用户和投票表连接起来。我在下面测试了这种方法。
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;
【讨论】:
SELECT id, 1 AS content_type_id, user_id FROM content_c
中的 1
可能是错字?
@okm:谢谢你说得对,应该是 3,我已经更新了。【参考方案3】:
我会使用预先计算的值来做到这一点。首先制作一个单独的表来存储每个用户收到的投票:
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
如果您的数据库中已有数据,您当然必须在第一次手动更新投票计数。
【讨论】:
以上是关于具体复杂的 SQL 查询和 Django ORM?的主要内容,如果未能解决你的问题,请参考以下文章
使用 filter() 和 Q 对象混合的 Django ORM 查询