Как обновить записи в Django: update(), save() и bulk_update()
Как обновить записи в Django ORM: save() с update_fields, QuerySet.update() и F(), bulk_update, update_or_create, select_for_update — с замерами SQL-запросов.
Содержание · 12 разделов
- Модели и замеры для примеров
- Какой способ выбрать
- save(): обновление одного объекта
- update_fields: только нужные колонки
- QuerySet.update(): один UPDATE на всю выборку
- Что update() не делает
- Ограничения update()
- Разные значения одним запросом: Case и When
- F-выражения: обновление без гонок
- bulk_update(): разные значения для многих объектов
- batch_size и лимит параметров
- Ограничения bulk_update()
- Обновить или вставить пачкой
- update_or_create(): обновить или создать
- select_for_update() и транзакции
- Обновление связанных объектов
- Асинхронное обновление: aupdate() и другие
- Частые вопросы
- Как сохранить только поля, изменённые в форме?
- Как обновить запись из словаря?
- Как получить обновлённые записи после update()?
- Почему не срабатывает post_save при массовом обновлении?
- Как проверить в тесте, сколько запросов делает обновление?
- Итог
Обновить записи в 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=...) в цикле | 1001 | 1000 | 8–28 с |
то же внутри transaction.atomic() | 1003 | 1000 | 0,3–0,4 с |
bulk_update(books, ["price"]) | 4 | 1 | 0,24–0,34 с |
bulk_update(..., batch_size=200) | 8 | 5 | 0,2–0,5 с |
update(price=F("price") + 1) | 1 | 1 | ~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=True | FOR UPDATE NOWAIT | не ждать: если строка занята, сразу DatabaseError |
skip_locked=True | FOR UPDATE SKIP LOCKED | пропустить занятые строки — удобно для очередей задач |
of=("self",) | FOR UPDATE OF "shop_book" | с select_related() блокировать только свою таблицу |
no_key=True | FOR 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-выражения
- #Транзакции