Массовое обновление записей в Django используя аннотации и подзапросы
Предисловие
В официальной документации Django нет информации как использовать функции update()
и annotate()
для обновления всех строк в QuerySet
используя аннотированное значение.
Сейчас мы покажем, как произвести такое обновление используя только функцию subquery()
из Django ORM без использования функции extra()
или SQL кода.
Модели
Для примера будем использовать код приложения блога из документации Django:
from django.db import models
class Blog(models.Model):
name = models.CharField(max_length=100)
rating = models.DecimalField(max_digits=3, decimal_places=2, default=5)
def __str__(self):
return self.name
class Entry(models.Model):
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
headline = models.CharField(max_length=255)
rating = models.IntegerField(default=5)
def __str__(self):
return self.headline
Проблема
Один из путей обновления рейтинга в записях блога, основанного на подсчете среднего значения всех голосов может быть такой:
from django.db.models import Avg
from blog.models import Blog
for blog in Blog.objects.annotate(avg_rating=Avg('entry__rating')):
blog.rating = blog.avg_rating or 0
blog.save()
Этот код может быть очень неэффективным и медленным, если у нас будет много записей блога и их оценок, потому что Django ORM выполняет SQL запрос для каждой итерации цикла.
Чтобы избежать вышеуказанных проблем и выполнить операцию обновления одним запросом SQL, мы могли бы попробовать следующий подход:
Blog.objects.update(rating=Avg('entry__rating'))
Но этот код не работает и выдаст ошибку:
Traceback (most recent call last): ... FieldError: Joined field references are not permitted in this query
Решение
Начиная с Django 1.11 появилась возможность использовать Django ORM с функцией subquery()
.
from django.db.models import Avg, OuterRef, Subquery
from blog.models import Blog, Entry
Blog.objects.update(
rating=Subquery(
Blog.objects.filter(
id=OuterRef('id')
).annotate(
avg_rating=Avg('entry__rating')
).values('avg_rating')[:1]
)
)
Например, в PostreSQL результат будет такой (как перевести проект Django с MySQL на PostgreSQL можно узнать в другой статье на нашем сайте):
UPDATE "blog_blog"
SET "rating" = (
SELECT AVG(U1."rating") AS "avg_rating"
FROM "blog_blog" U0
LEFT OUTER JOIN "blog_entry" U1 ON (U0."id" = U1."blog_id")
WHERE U0."id" = ("blog_blog"."id")
GROUP BY U0."id"
LIMIT 1
)
Перевод статьи https://www.paulox.net/2018/10/01/updating-a-django-queryset-with-annotation-and-subquery/
Вернуться на верх