Django 使用注释更新查询集

Posted

技术标签:

【中文标题】Django 使用注释更新查询集【英文标题】:Django update queryset with annotation 【发布时间】:2011-04-08 20:09:21 【问题描述】:

我想使用带注释的值更新查询集中的所有行。

我有一个简单的模型:

class Relation(models.Model):
    rating = models.IntegerField(default=0)

class SignRelation(models.Model):
    relation = models.ForeignKey(Relation, related_name='sign_relations')
    rating = models.IntegerField(default=0)

我想避免这段代码:

for relation in Relation.objects.annotate(total_rating=Sum('sign_relations__rating')):
    relation.rating = relation.total_rating or 0
    relation.save()

并使用以下内容在 一个 SQL 请求中进行更新:

Relation.objects.update(rating=Sum('sign_relations__rating'))

不起作用:

TypeError: int() argument must be a string or a number, not 'Sum'

Relation.objects.annotate(total_rating=Sum('sign_relations__rating')).update(rating=F('total_rating'))

也不行:

DatabaseError: missing FROM-clause entry for table "relations_signrelation"
LINE 1: UPDATE "relations_relation" SET "rating" = SUM("relations_si...

是否可以为此目的使用 Django 的 ORM?文档中没有关于同时使用 update()annotate() 的信息。

【问题讨论】:

我不确定这在纯 SQL 中是否可行?在 SQL 中进行 UPDATE 时,我认为不可能加入其他表。 是的,这是可能的 - 通过子查询,例如。 UPDATE t1 SET a = (SELECT SUM(c) from t2 where t2.b=t1.b); 你真正想做什么?您是否尝试总结“SigningRelation”表中“rating”列的所有值,然后将其保存为“Relation”表中的新行? 【参考方案1】:

对于 Django 1.11+,您可以使用 Subquery:

from django.db.models import OuterRef, Subquery, Sum

Relation.objects.update(
    rating=Subquery(
        Relation.objects.filter(
            id=OuterRef('id')
        ).annotate(
            total_rating=Sum('sign_relations__rating')
        ).values('total_rating')[:1]
    )
)

此代码生成与Tomasz Jakub Rup 提出的相同的SQL 代码,但没有使用RawSQL 表达式。 由于SQL injection) 的可能性,Django 文档警告不要使用 RawSQL。

更新

我根据这个答案发表了一篇文章并进行了更深入的解释:Updating a Django queryset with annotation and subquery on paulox.net

【讨论】:

哇,updateSubquery 超级酷,感谢您指出这种模式! 很高兴您发现我的回答很有用 我建议使用.values_list('total_rating', flat=True) 直接提取值,而不是使用我一开始不理解的[:1] 这是一个很棒的 sn-p,但我通过了解释,查询计划在一个相当大的表上似乎很慢。另一种选择是使用 ORM 生成 subqery SQL,使用数据创建临时表,然后编写手动更新查询。在我的情况下,它需要多分钟的更新时间才 1 分钟。 我在你的博客上也注意到了这一点,有一个新包pypi.org/project/django-sql-utils 可以让你更轻松地进行子查询Relation.objects.update(rating=SubqueryAvg('sign_relations__rating'))【参考方案2】:

UPDATE 语句不支持GROUP BY。参见例如PostgreSQL Docs,SQLite Docs。

你需要这样的东西:

UPDATE relation
SET rating = (SELECT SUM(rating)
              FROM sign_relation
              WHERE relation_id = relation.id)

DjangoORM 中的等价物:

from django.db.models.expressions import RawSQL

Relation.objects.all(). \
    update(rating=RawSQL('SELECT SUM(rating) FROM signrelation WHERE relation_id = relation.id', []))

或:

from django.db.models import F, Sum
from django.db.models.expressions import RawSQL

Relation.objects.all(). \
    update(rating=RawSQL(SignRelation.objects. \
                         extra(where=['relation_id = relation.id']). \
                         values('relation'). \
                         annotate(sum_rating=Sum('rating')). \
                         values('sum_rating').query, []))

【讨论】:

您是否能够运行上一次推荐? .filter(relation=F('relation__pk')) 不等于 WHERE (relation.id = relation.id),因此不加入任何东西吗? 感谢更新。 .extra() 子句是金子!【参考方案3】:

postgres 的解决方法:

with connection.cursor() as cursor:
    sql, params = qs.query.sql_with_params()
    cursor.execute("""
        WITH qs AS ()
        UPDATE foo SET bar = qs.bar
        FROM qs WHERE qs.id = foo.id
    """.format(sql), params)

【讨论】:

谁给了你一个减号,仅仅因为使用 mysql?太刻薄了。【参考方案4】:

您可以定义自己的自定义对象管理器:

class RelationManager(models.Manager):
    def annotated(self,*args,*kwargs):
         queryset = super(RelationManager,self).get_queryset()
         for obj in queryset:
               obj.rating = ... do something ...
         return queryset

class Relations(models.Model):
    rating = models.IntegerField(default=0)
    rating_objects = RelationManager()

然后在你的代码中:

q = Realation.rating_objects.annotated()

添加 args/kwargs 以自定义此管理器返回的内容。

【讨论】:

【参考方案5】:

如果你想避免多次调用数据库,你应该使用transaction.atomic

阅读有关 Django 文档的更多信息:https://docs.djangoproject.com/en/1.9/topics/db/transactions/#controlling-transactions-explicitly

【讨论】:

transaction.atomic 不会减少调用次数,它只是强制整个事务要么完全成功,要么失败而不做任何修改。【参考方案6】:

你真的不能这样做。看看the code 了解update 和follow it through 了解一些精读。

老实说,在 Manager 定义中放置这样的内容有什么问题?将您不想放在视图中的那 3 行放入经理中,必要时调用该经理。此外,您所做的“魔术”要少得多,当下一个开发人员查看您的代码时,他们将不必求助于几个 WTF .. :)

另外,我很好奇,看起来你可以使用SQL Join with UPDATE statements,但这是一些经典的 SQL 黑客技术。所以如果你愿意,你可以使用 Django 的原始 SQL 功能;)

【讨论】:

“老实说,在 Manager 定义中放置这样的内容有什么问题?”这可能是发送到数据库的 lot 请求。他特别要求“一个 SQL 请求”。另外,我认为他尝试的示例很容易阅读,根本不像魔术。 最近我不得不手动编写一个 SQL 查询,因为正确编写它需要 200 毫秒才能执行,而使用更新外观时我必须等待 30 分钟。这就是原因。

以上是关于Django 使用注释更新查询集的主要内容,如果未能解决你的问题,请参考以下文章

Django:在请求值时从查询集注释字段中删除小数前缀

(转)解决swagger跨项目或跨程序集注释不显示问题

为查询集注释排名字段的正确方法

将更新签入到 TFS 后编辑变更集注释

TFS 签入时,提示“变更集注释策略 中的内部错误……”

Django 使用注释更新查询集