Django-Admin.ru

Как обновить записи в Django: update(), save() и bulk_update()

Как обновить записи в Django ORM: save() с update_fields, QuerySet.update() и F(), bulk_update, update_or_create, select_for_update — с замерами SQL-запросов.

Редакция Django-Admin.ru
· 19 мин чтения

Содержание · 12 разделов
  1. Модели и замеры для примеров
  2. Какой способ выбрать
  3. save(): обновление одного объекта
  4. update_fields: только нужные колонки
  5. QuerySet.update(): один UPDATE на всю выборку
  6. Что update() не делает
  7. Ограничения update()
  8. Разные значения одним запросом: Case и When
  9. F-выражения: обновление без гонок
  10. bulk_update(): разные значения для многих объектов
  11. batch_size и лимит параметров
  12. Ограничения bulk_update()
  13. Обновить или вставить пачкой
  14. update_or_create(): обновить или создать
  15. select_for_update() и транзакции
  16. Обновление связанных объектов
  17. Асинхронное обновление: aupdate() и другие
  18. Частые вопросы
  19. Как сохранить только поля, изменённые в форме?
  20. Как обновить запись из словаря?
  21. Как получить обновлённые записи после update()?
  22. Почему не срабатывает post_save при массовом обновлении?
  23. Как проверить в тесте, сколько запросов делает обновление?
  24. Итог

Обновить записи в Django ORM можно тремя основными способами. Один объект — изменить атрибут и вызвать save(), лучше с update_fields. Много строк одним значением или формулой — QuerySet.update(): один SQL-запрос UPDATE без загрузки объектов. Много объектов с разными значениями — bulk_update():

book.price = Decimal("1190.00")
book.save(update_fields=["price"])                              # UPDATE одной колонки

Book.objects.filter(stock=0).update(is_active=False)            # один UPDATE на все строки
Book.objects.filter(pk=book.pk).update(views=F("views") + 1)    # счётчик без гонки

Book.objects.bulk_update(books, ["price"], batch_size=500)      # разные цены, UPDATE ... CASE

Главное отличие: save() вызывает переопределённый метод модели, сигналы pre_save/post_save и обновляет поля auto_now, а update() и bulk_update() — нет. Ниже — как работает каждый способ, сколько SQL-запросов он делает (все цифры — замеры) и где его ограничения. Общий обзор QuerySet — в шпаргалке по Django ORM.

Модели и замеры для примеров

Примеры проверены на Django 6.1.2, Python 3.12 и SQLite. Модель переопределяет save() и подписана на post_save — по выводу видно, какие методы срабатывают:

from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver


class Author(models.Model):
    name = models.CharField(max_length=200)
    books_count = models.PositiveIntegerField(default=0)


class Book(models.Model):
    title = models.CharField(max_length=300)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
    price = models.DecimalField(max_digits=8, decimal_places=2)
    stock = models.PositiveIntegerField(default=0)
    views = models.PositiveIntegerField(default=0)
    is_active = models.BooleanField(default=True)
    updated_at = models.DateTimeField(auto_now=True)

    def save(self, **kwargs):
        print(f"save() вызван для «{self.title}»")
        super().save(**kwargs)


@receiver(post_save, sender=Book)
def log_book_saved(sender, instance, created, update_fields, **kwargs):
    print(f"post_save: {instance.title}, update_fields={update_fields}")

В базе три книги: «Война и мир» и «Анна Каренина» Толстого, «Идиот» Достоевского. Запросы считаются через CaptureQueriesContext — как им пользоваться, показано в статье про N+1 в Django ORM.

Какой способ выбрать

СпособSQL-запросов (замер)save() и сигналыauto_nowКогда использовать
obj.save()1 UPDATE всех колонок (+1 SELECT на загрузку)даобновляетсяодин объект, нужна логика модели
obj.save(update_fields=[...])1 UPDATE указанных колонокдатолько если поле в спискеодин объект, частичное изменение
QuerySet.update()1 UPDATEнетнет, передайте явноодно значение или формула для многих строк
update() + F()1 UPDATEнетнетсчётчики, остатки, расчёт от текущего значения
bulk_update()1 UPDATE на пачку + BEGIN/COMMITнетнетразные значения, посчитанные в Python
update_or_create()SELECT + UPDATE или INSERT в транзакциидаобновляетсясинхронизация по ключу: обновить или создать
select_for_update() + save()SELECT … FOR UPDATE + UPDATEдакак у save()проверка условия перед записью без гонки

Замер на 1000 книгах: каждой нужно поднять цену на 1. Время — порядок величин на SQLite в Windows, оно сильно колебалось между прогонами; количество запросов стабильно:

СпособВсего запросовИз них UPDATEВремя
save(update_fields=...) в цикле100110008–28 с
то же внутри transaction.atomic()100310000,3–0,4 с
bulk_update(books, ["price"])410,24–0,34 с
bulk_update(..., batch_size=200)850,2–0,5 с
update(price=F("price") + 1)11~0,01 с

Цикл с save() в режиме автокоммита медленнее всего: SQLite фиксирует на диск каждый из 1000 запросов. На сетевой СУБД (PostgreSQL, MySQL) к этому добавляется сетевая задержка на каждый запрос, поэтому сокращение числа запросов важнее, чем на локальном SQLite. Если новое значение вычисляется формулой, update() с F() выигрывает у всех.

save(): обновление одного объекта

Загрузили объект, изменили атрибут, вызвали save(). Если у объекта есть первичный ключ, Django выполняет UPDATE всех колонок:

book = Book.objects.get(title="Война и мир")
book.price = Decimal("1090.00")
book.save()
save() вызван для «Война и мир»
post_save: Война и мир, update_fields=None
# запросов: 1
UPDATE "shop_book" SET "title" = 'Война и мир', "author_id" = 1, "price" = '1090.00', "stock" = 5, "views" = 0, "is_active" = 1, "updated_at" = '2026-10-10 00:20:17.156334' WHERE "shop_book"."id" = 1

Вместе с get() это два запроса. Вариант с загрузкой нужен, когда важна логика модели: переопределённый save(), сигналы, auto_now. Как правильно переопределять save() и почему валидация при нём не запускается, разобрано в статье про модели Django.

update_fields: только нужные колонки

update_fields ограничивает UPDATE перечисленными полями:

book.price = Decimal("1190.00")
book.save(update_fields=["price"])
save() вызван для «Война и мир»
post_save: Война и мир, update_fields=frozenset({'price'})
# запросов: 1
UPDATE "shop_book" SET "price" = '1190.00' WHERE "shop_book"."id" = 1

Полный save() перезаписывает все колонки значениями из памяти. Если другой процесс успел изменить stock, пока объект лежал в памяти, его изменение пропадёт. С update_fields вы трогаете только своё поле.

Что важно знать про update_fields:

  • auto_now не обновится, если поле не в списке: Django вызывает подготовку значения (pre_save()) только для перечисленных полей. Нужна дата изменения — добавьте её явно: save(update_fields=["price", "updated_at"]).
  • Обработчик post_save получает аргумент update_fields — по нему можно пропустить лишнюю работу, например не пересчитывать поисковый индекс при смене цены.
  • С пустым списком запроса не будет и post_save не отправится, хотя переопределённый save() выполнится. Вместо списка можно передать любой итерируемый объект со строками, например ключи словаря: save(update_fields=data.keys()).
  • Если строки в базе уже нет, Django 6.0+ бросает Book.NotUpdated с текстом Save with update_fields did not affect any rows. Исключение наследует DatabaseError, так что старый код с except DatabaseError продолжит работать.
  • Объект, загруженный через only() или defer(), при обычном save() сохраняет только загруженные поля. Для Book.objects.only("title", "price") получился UPDATE ... SET "title" = ..., "price" = ... — без updated_at.

QuerySet.update(): один UPDATE на всю выборку

update() превращает QuerySet в один SQL-запрос и возвращает число строк:

n = Book.objects.filter(stock=0).update(is_active=False)
print(n)  # → 1
# запросов: 1
UPDATE "shop_book" SET "is_active" = 0 WHERE "shop_book"."stock" = 0

Строки save() вызван и post_save в выводе нет. Объекты не создаются, данные не загружаются в память. Условия пишутся как в filter(), включая связи — Django соберёт подзапрос:

Book.objects.filter(author__name__startswith="Фёдор").update(views=F("views") + 1)
UPDATE "shop_book" SET "views" = ("shop_book"."views" + 1) WHERE "shop_book"."id" IN (SELECT "U0"."id" FROM "shop_book" "U0" INNER JOIN "shop_author" "U1" ON ("U0"."author_id" = "U1"."id") WHERE "U1"."name" LIKE 'Фёдор%' ESCAPE '\')

Все операторы поиска и Q-объекты для условий — в статье про filter() и exclude().

Что update() не делает

  • Не вызывает save() модели — логика из переопределённого метода не выполнится.

  • Не отправляет pre_save и post_save. Если от сигнала зависит кеш или поисковый индекс, обновите их вручную или переберите объекты с save().

  • Не обновляет auto_now. В замере updated_at после update() остался прежним. Передавайте время явно:

    from django.utils import timezone
    
    Book.objects.filter(stock=0).update(is_active=False, updated_at=timezone.now())
  • Не валидирует данные: ни full_clean(), ни choices.

  • Не меняет объекты в памяти. Загруженный раньше объект хранит старое значение, пока вы его не перечитаете:

    book = Book.objects.get(title="Идиот")
    Book.objects.filter(pk=book.pk).update(price=Decimal("700.00"))
    print(book.price)                       # → 640.00
    book.refresh_from_db(fields=["price"])
    print(book.price)                       # → 700.00

update() возвращает число строк, подошедших под условие, включая те, где значение уже было таким: повторный update(is_active=False) на выключенной книге вернул 1. Если ничего не нашлось — 0, исключения нет.

Ограничения update()

Обновлять можно только колонки основной таблицы модели. Три ошибки из замеров:

Book.objects.update(author__name="X")
# → FieldDoesNotExist: Book has no field named 'author__name'

Book.objects.update(title=F("author__name"))
# → FieldError: Joined field references are not permitted in this query

Book.objects.all()[:2].update(is_active=True)
# → TypeError: Cannot update a query once a slice has been taken.

Чтобы обновить первые N строк, выберите их ключи и обновите по pk__in. Как подставить значение из другой таблицы, показано ниже в разделе про связанные объекты.

На MySQL и MariaDB update() с фильтром по связанной таблице может выполниться как SELECT и затем UPDATE, и между ними данные могут измениться — в таких случаях оборачивайте вызов в транзакцию. Там же работает order_by() перед update(): это нужно, когда вы сдвигаете значения уникального поля. Другие СУБД order_by() в update() игнорируют.

Разные значения одним запросом: Case и When

Если новое значение зависит от условия, bulk_update() не нужен — хватит update() с Case. Скидка 20 % на книги без остатка и 10 % на книги с остатком больше 10:

from django.db.models import Case, F, When

Book.objects.update(
    price=Case(
        When(stock=0, then=F("price") * Decimal("0.8")),
        When(stock__gt=10, then=F("price") * Decimal("0.9")),
        default=F("price"),
    )
)
# → 1 запрос; цены: Война и мир 990.00, Анна Каренина 600.00, Идиот 576.00

Не забывайте default: без него строки, не попавшие ни в одно условие, получат NULL. Для обязательного поля это ошибка IntegrityError: NOT NULL constraint failed: shop_book.price, для поля с null=True — тихая потеря данных.

F-выражения: обновление без гонок

Код «прочитать — изменить в Python — сохранить» теряет данные при параллельных запросах. Два запроса загрузили книгу с остатком 12 и оба списали по экземпляру:

a = Book.objects.get(title="Идиот")
b = Book.objects.get(title="Идиот")
a.stock -= 1
a.save()
b.stock -= 1
b.save()
print(Book.objects.get(title="Идиот").stock)  # → 11: одно списание потерялось

F("stock") ссылается на значение в базе в момент выполнения UPDATE, поэтому вычитание делает СУБД и второй запрос видит результат первого:

from django.db.models import F

Book.objects.filter(pk=a.pk).update(stock=F("stock") - 1)
Book.objects.filter(pk=b.pk).update(stock=F("stock") - 1)
print(Book.objects.get(title="Идиот").stock)  # → 10

Проверку «хватает ли остатка» тоже можно отдать базе. Условие в filter() и списание выполняются одним запросом, а число обновлённых строк показывает результат:

updated = Book.objects.filter(pk=book.pk, stock__gte=1).update(stock=F("stock") - 1)
if not updated:
    raise ValueError("Нет в наличии")
# stock = 12 → updated = 1; stock = 0 → updated = 0

F() можно присвоить и атрибуту объекта перед save(). С Django 6.0 значение сразу перечитывается: на SQLite, PostgreSQL и Oracle — через RETURNING в том же запросе, на MySQL и MariaDB — отдельным запросом при первом обращении к полю:

book.views = F("views") + 1
book.save(update_fields=["views"])
print(book.views)  # → 1, без дополнительного запроса
UPDATE "shop_book" SET "views" = ("shop_book"."views" + 1) WHERE "shop_book"."id" = 3 RETURNING "shop_book"."views"

Важно: в Django 5.2 LTS и раньше после такого save() в атрибуте остаётся выражение F(), и каждый следующий save() снова прибавит единицу. Там после присваивания F() вызывайте book.refresh_from_db(fields=["views"]).

bulk_update(): разные значения для многих объектов

Когда значения посчитаны в Python (курс из API, перевод, данные из CSV) и у каждой строки свои, update() не подходит. bulk_update() собирает изменения в один UPDATE с CASE:

books = list(Book.objects.order_by("pk"))
prices = {"Война и мир": "1200.00", "Анна Каренина": "800.00", "Идиот": "650.00"}
for book in books:
    book.price = Decimal(prices[book.title])

n = Book.objects.bulk_update(books, ["price"])
print(n)  # → 3
# запросов: 3
BEGIN
UPDATE "shop_book" SET "price" = (CAST(CASE WHEN ("shop_book"."id" = 1) THEN (CAST('1200.00' AS REAL)) WHEN ("shop_book"."id" = 2) THEN (CAST('800.00' AS REAL)) WHEN ("shop_book"."id" = 3) THEN (CAST('650.00' AS REAL)) ELSE NULL END AS NUMERIC)) WHERE "shop_book"."id" IN (1, 2, 3)
COMMIT

Пачки выполняются в одной транзакции: если упадёт третья, первые две откатятся.

batch_size и лимит параметров

batch_size задаёт, сколько объектов попадёт в один UPDATE. По умолчанию Django старается уложить всё в одну пачку, а для SQLite и Oracle сам ограничивает её размер из-за лимита на число параметров запроса.

На SQLite этот расчёт может не сработать. В замере на Django 6.1.2 и SQLite 3.49 bulk_update() 10 000 объектов без batch_size прошёл для одного поля, но с двумя полями упал:

Book.objects.bulk_update(books, ["price", "stock"])
# → OperationalError: too many SQL variables

Book.objects.bulk_update(books, ["price", "stock"], batch_size=1000)
# → 10 запросов UPDATE, без ошибки

Указывайте batch_size явно (500–1000 — разумный старт) — так размер запроса не зависит от СУБД.

Ограничения bulk_update()

  • Не вызывает save(), сигналы pre_save/post_save и не обновляет auto_now. Нужна дата изменения — присвойте updated_at = timezone.now() каждому объекту и добавьте поле в список.
  • Список полей обязателен: bulk_update(books, []) → ValueError: Field names must be given to bulk_update().
  • Первичный ключ обновить нельзя: ValueError: bulk_update() cannot be used with primary key fields.
  • Все объекты должны уже существовать: новый объект без pk → ValueError: All bulk_update() objects must have a primary key set.
  • Поля связанных моделей недоступны: "author__name" → FieldDoesNotExist.
  • Если один и тот же объект попал в пачку дважды, применится первый экземпляр. В замере два экземпляра «Идиота» со stock = 1 и stock = 2 дали в базе 1, функция вернула 1.
  • Все WHEN для всех пачек готовятся в памяти до первого запроса. Для сотен тысяч строк загружайте и обновляйте данные частями, например по 1000 ключей.
  • Поля, унаследованные от родителя при многотабличном наследовании, добавляют по запросу на каждого предка.

Если все строки получают одно значение или значение считается формулой, bulk_update() избыточен — используйте update().

Обновить или вставить пачкой

Если в наборе есть и новые, и существующие строки, используйте bulk_create() с update_conflicts=True. Это один запрос INSERT ... ON CONFLICT DO UPDATE (в MySQL и MariaDB — ON DUPLICATE KEY UPDATE), Oracle такой режим не поддерживает:

Book.objects.bulk_create(
    rows,
    update_conflicts=True,
    unique_fields=["id"],
    update_fields=["price"],
)
# запросов: 1
INSERT INTO "shop_book" (...) VALUES (1, 'Война и мир', ...), (10, 'Бесы', ...) ON CONFLICT("id") DO UPDATE SET "price" = EXCLUDED."price" RETURNING "shop_book"."id"

Книга с id=1 получила новую цену, книга с id=10 добавилась. На PostgreSQL и SQLite аргумент unique_fields обязателен, и эти поля должны быть покрыты уникальным ограничением. На MySQL и MariaDB его не передают — конфликт определяет любой уникальный индекс, а с unique_fields Django бросит NotSupportedError.

update_or_create(): обновить или создать

update_or_create() ищет объект по именованным аргументам, обновляет поля из defaults или создаёт новый. Аргумент create_defaults (Django 5.0+) задаёт значения только для создания — например, начальный остаток, который нельзя затирать при каждой синхронизации:

book, created = Book.objects.update_or_create(
    title="Идиот",
    author=dost,
    defaults={"price": Decimal("700.00")},
    create_defaults={"price": Decimal("700.00"), "stock": 1},
)
print(created, book.price, book.stock)  # → False 700.00 12
save() вызван для «Идиот»
post_save: Идиот, update_fields=frozenset({'updated_at', 'price'})
# запросов: 4
BEGIN
SELECT "shop_book"."id", … FROM "shop_book" WHERE ("shop_book"."author_id" = 2 AND "shop_book"."title" = 'Идиот') LIMIT 21
UPDATE "shop_book" SET "price" = '700.00', "updated_at" = '2026-10-10 00:13:52.197259' WHERE "shop_book"."id" = 3
COMMIT

Для «Бесов», которых в базе не было, метод вернул created=True и stock=1 за 6 запросов: BEGIN, SELECT, SAVEPOINT, INSERT, RELEASE SAVEPOINT, COMMIT.

Как устроен метод:

  • Всё выполняется в transaction.atomic(), а строка выбирается через select_for_update() — на PostgreSQL, MySQL и Oracle параллельный update_or_create() по той же записи ждёт окончания транзакции.
  • Существующий объект сохраняется через save(update_fields=...): в список попадают поля из defaults и поля с auto_now. Сигналы и переопределённый save() срабатывают.
  • Значения в defaults могут быть вызываемыми: defaults={"stock": lambda: 42}.
  • Если по условиям нашлось несколько строк — MultipleObjectsReturned. Ищите по полям, которые однозначно определяют запись.
  • Защита от дублей при одновременном создании — только уникальное ограничение в базе (unique=True или UniqueConstraint). Без него два параллельных вызова могут вставить две одинаковые строки.

select_for_update() и транзакции

F() закрывает гонку, когда изменение укладывается в одну формулу. Если перед записью нужна логика в Python — проверить остаток, посчитать скидку, записать связанную строку, — заблокируйте строку на время транзакции:

from django.db import transaction


def buy(book_id, qty):
    with transaction.atomic():
        book = Book.objects.select_for_update().get(pk=book_id)
        if book.stock < qty:
            raise ValueError("Недостаточно на складе")
        book.stock -= qty
        book.save(update_fields=["stock", "updated_at"])
    return book.stock

На PostgreSQL get() превращается в такой запрос:

SELECT "shop_book"."id", … FROM "shop_book" WHERE "shop_book"."id" = 3 LIMIT 21 FOR UPDATE

Вторая транзакция, которая выбирает ту же строку с select_for_update(), ждёт, пока первая не завершится, и потом читает уже новый остаток. На SQLite вызов занял 4 запроса (BEGIN, SELECT, UPDATE, COMMIT), но FOR UPDATE в SQL нет: SQLite не поддерживает построчную блокировку и игнорирует select_for_update() без ошибки. Проверять такую логику нужно на той СУБД, что работает в продакшне — например, на PostgreSQL.

Параметры:

ПараметрSQL на PostgreSQLЧто делает
nowait=TrueFOR UPDATE NOWAITне ждать: если строка занята, сразу DatabaseError
skip_locked=TrueFOR UPDATE SKIP LOCKEDпропустить занятые строки — удобно для очередей задач
of=("self",)FOR UPDATE OF "shop_book"с select_related() блокировать только свою таблицу
no_key=TrueFOR NO KEY UPDATEболее слабая блокировка, только PostgreSQL

nowait и skip_locked вместе передать нельзя — будет ValueError. MySQL поддерживает nowait, skip_locked и of, MariaDB — только nowait и skip_locked.

Типичные ошибки:

  • Вызов вне atomic(). На PostgreSQL, MySQL и Oracle это TransactionManagementError: select_for_update cannot be used outside of a transaction. На SQLite ошибки не будет, поэтому её легко пропустить в разработке.
  • Тест в TestCase. TestCase оборачивает каждый тест в транзакцию, и вызов вне atomic() пройдёт без ошибки. Для проверки блокировок используйте TransactionTestCase.
  • select_related() по необязательной связи (null=True) — NotSupportedError: FOR UPDATE cannot be applied to the nullable side of an outer join. Добавьте exclude(поле=None) или of=("self",).

Обновление связанных объектов

Внешний ключ обновляется через update() экземпляром или его id:

Book.objects.filter(title="Идиот").update(author=tolstoy)
Book.objects.filter(title="Идиот").update(author_id=dost.pk)
# → UPDATE "shop_book" SET "author_id" = 2 WHERE "shop_book"."title" = 'Идиот'

Обратный менеджер связи — это тоже QuerySet, у него есть update():

tolstoy.books.update(is_active=False)
# → UPDATE "shop_book" SET "is_active" = 0 WHERE "shop_book"."author_id" = 1

Чтобы изменить поле связанного объекта, сохраните сам этот объект — book.save() автора не трогает:

book = Book.objects.select_related("author").get(title="Идиот")
book.author.name = "Ф. М. Достоевский"
book.author.save(update_fields=["name"])
# → UPDATE "shop_author" SET "name" = 'Ф. М. Достоевский' WHERE "shop_author"."id" = 2

Значение из другой таблицы в update() через F("author__name") подставить нельзя, агрегат Count("books") тоже вызывает FieldError. Используйте Subquery — пример пересчитывает число книг у каждого автора одним запросом:

from django.db.models import Count, OuterRef, Subquery
from django.db.models.functions import Coalesce

Author.objects.update(
    books_count=Coalesce(
        Subquery(
            Book.objects.filter(author=OuterRef("pk"))
            .values("author")
            .annotate(c=Count("pk"))
            .values("c")
        ),
        0,
    )
)
# → 1 запрос; [('Лев Толстой', 2), ('Ф. М. Достоевский', 1)]

Coalesce превращает NULL в 0 для авторов без книг. Связи «многие ко многим» через update() не меняются — для них у менеджера связи есть add(), remove() и set(). Подробнее о связях — в статье про ForeignKey в Django.

Асинхронное обновление: aupdate() и другие

У каждого метода записи есть асинхронная версия с префиксом a:

async def sync_catalog():
    n = await Book.objects.filter(stock=0).aupdate(is_active=False)

    book = await Book.objects.aget(title="Идиот")
    book.price = Decimal("650.00")
    await book.asave(update_fields=["price", "updated_at"])

    books = [b async for b in Book.objects.filter(author=tolstoy)]
    for b in books:
        b.stock += 1
    await Book.objects.abulk_update(books, ["stock"])

    await Book.objects.aupdate_or_create(
        title="Бесы", author=dost, defaults={"price": Decimal("500.00")}
    )
    await book.arefresh_from_db()

Транзакции в асинхронном режиме пока не работают, поэтому transaction.atomic() и select_for_update() из корутины не вызвать. Код с блокировками пишут обычной функцией и вызывают через sync_to_async:

from asgiref.sync import sync_to_async

stock = await sync_to_async(buy)(book_id=3, qty=2)

aupdate_or_create() внутри устроен так же: он вызывает синхронный update_or_create() через sync_to_async, поэтому его транзакция работает. Основы async/await — в статье про асинхронность в Python.

Частые вопросы

Как сохранить только поля, изменённые в форме?

У ModelForm есть список changed_data. Сохраните объект без записи в базу и передайте этот список в update_fields:

if form.is_valid() and form.has_changed():
    book = form.save(commit=False)
    book.save(update_fields=[*form.changed_data, "updated_at"])

Для формы с полями title, price и stock, где изменилась только цена, changed_data равен ['price'], и получился один запрос UPDATE ... SET "price" = ..., "updated_at" = .... Поля «многие ко многим» в update_fields передавать нельзя (ValueError): исключите их из списка и сохраните через form.save_m2m(). Как устроены формы для редактирования — в статье про формы Django.

Как обновить запись из словаря?

Если сигналы и save() не нужны, распакуйте словарь в update(): Book.objects.filter(pk=pk).update(**data). Если нужны — присвойте атрибуты и сохраните ключи словаря:

for field, value in data.items():
    setattr(book, field, value)
book.save(update_fields=data.keys())

Оба варианта дают один UPDATE с теми же колонками. Ключи берите из проверенного источника — например, form.cleaned_data: неизвестное имя поля вызовет FieldDoesNotExist в update() и ValueError в save().

Как получить обновлённые записи после update()?

update() возвращает только число строк — RETURNING для него в ORM нет. Перечитайте данные отдельным запросом: refresh_from_db() для одного объекта или Book.objects.filter(pk__in=ids) для набора. Если вы обновляли по условию, которое после обновления перестанет выполняться, сначала сохраните ключи: ids = list(qs.values_list("pk", flat=True)).

Почему не срабатывает post_save при массовом обновлении?

update(), bulk_update() и bulk_create() работают на уровне SQL и сигналы не отправляют. Варианты: перебрать объекты и вызвать save() (N запросов), вынести логику из обработчика в функцию и вызвать её после массового обновления или отправить сигнал вручную через post_save.send(...) для каждого объекта. Последний вариант усложняет код — обычно проще вызвать функцию явно.

Как проверить в тесте, сколько запросов делает обновление?

assertNumQueries падает, если запросов больше или меньше ожидаемого:

def test_bulk_price_update(self):
    with self.assertNumQueries(1):
        Book.objects.filter(stock=0).update(is_active=False)

Учтите, что BEGIN и COMMIT от bulk_update() на SQLite тоже попадают в счётчик: в замере их было 3 на одну пачку.

Итог

  • Один объект — save(update_fields=[...]): сработают сигналы и логика модели, а в UPDATE попадут только нужные колонки. Поле auto_now добавляйте в список явно.
  • Много строк одним значением или формулой — QuerySet.update(): один запрос, но без save(), сигналов и auto_now.
  • Счётчики и остатки — F() внутри update(), а проверку условия кладите в filter().
  • Разные значения из Python — bulk_update() с явным batch_size; смесь новых и существующих строк — bulk_create(update_conflicts=True).
  • «Обновить или создать» по ключу — update_or_create() с create_defaults и уникальным ограничением в базе.
  • Логика между чтением и записью — select_for_update() внутри transaction.atomic() на PostgreSQL или MySQL.

Полный список методов QuerySet — в документации Django.

  • #Django ORM
  • #QuerySet
  • #update
  • #bulk_update
  • #F-выражения
  • #Транзакции
↑ ↓ выборEnter открытьEsc закрыть