Подтвердить что ты не робот

Django updatesetset с аннотацией

Я хочу обновить все строки в наборе запросов, используя аннотированное значение.

У меня есть простые модели:

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() вместе в документах.

4b9b3361

Ответ 1

Для Django 1. 11+ вы можете использовать подзапрос:

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

Этот код создает тот же код SQL, предложенный Tomasz Jakub Rup, но без использования выражения RawSQL (документация Django предупреждает вас об использовании его, поскольку SQL-инъекция).

Обновить

Я опубликовал статью, основанную на этом ответе, с более подробными пояснениями:

"Обновление набора запросов Django с аннотацией и подзапросом" на paulox.net

Ответ 2

Оператор

UPDATE не поддерживает GROUP BY. См. Документы PostgreSQL, 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, []))

Ответ 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)

Ответ 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

Вы действительно не можете этого сделать. Посмотрите код для update и следовать за ним для некоторого тонкого чтения.

Честно говоря, что случилось с размещением чего-то подобного в определении менеджера? Поместите эти 3 строки, которые вы не хотите отображать в своем представлении, в менеджер, при необходимости вызовите этого менеджера. Кроме того, вы делаете гораздо меньше "магии", и когда следующий разработчик смотрит на ваш код, им не придется прибегать к нескольким WTF..:)

Кроме того, мне было любопытно, и похоже, вы можете использовать SQL Присоединиться к операторам UPDATE, но это классический хакер SQL. Поэтому, если вы 'так склонны, вы можете использовать Djangos raw SQL для этого;)