Обновление и удаление в Django ORM | Курс Django ORM урок 2.5
Цель урока
Разобрать все способы обновления и удаления данных в Django ORM. Понять разницу между save() и update() на уровне QuerySet, между delete() на объекте и на QuerySet. Изучить bulk_create() и bulk_update(), и увидеть реальную разницу в производительности.
Необходимые знания
Обновление через save()
Мы уже разбирали save() в уроке 2.1. Напомню ключевой момент:
product = Product.objects.get(id=1)
product.price = Decimal("899.99")
product.save()
-- Обновляет ВСЕ поля модели
UPDATE shop_product
SET name = 'iPhone 15',
slug = 'iphone-15',
description = '...',
price = 899.99,
stock = 42,
is_active = true,
updated_at = NOW(),
category_id = 3
WHERE id = 1;
Проблемы:
- Сначала SELECT (чтобы получить объект), потом UPDATE - 2 запроса
- UPDATE обновляет все поля, даже неизменившиеся
- Race condition: если между SELECT и UPDATE другой процесс изменил
stock, изменение будет перезаписано
save(update_fields=["price", "updated_at"]) решает пункты 2 и частично 3, но 2 запроса остаются.
QuerySet.update() - обновление без загрузки объектов
update() обновляет строки напрямую через SQL UPDATE без SELECT и без создания объектов Python:
# Снизить цену всех продуктов категории "phones" на 10%
from decimal import Decimal
Product.objects.filter(category__slug="phones").update(
price=Decimal("899.99") # конкретное значение
)
UPDATE shop_product
SET price = 899.99
WHERE category_id = (SELECT id FROM shop_category WHERE slug = 'phones');
Один запрос. Нет отдельного SELECT для загрузки объектов, нет объектов Python в памяти, нет проблемы с race condition на неизменяемых полях.
update() возвращает количество обновленных строк:
updated_count = Product.objects.filter(stock=0).update(is_active=False)
print(f"Деактивировано {updated_count} продуктов")
Ограничения update()
Не вызывает никаких сигналов. pre_save, post_save и любые другие сигналы не срабатывают. Если у вас есть обработчики этих сигналов, они будут проигнорированы.
Не вызывает save() модели. Если в модели переопределен save() с бизнес-логикой, она не выполнится.
Не обновляет auto_now поля автоматически. updated_at = models.DateTimeField(auto_now=True) обновляется только через save(). При update() нужно передать значение явно:
from django.utils import timezone
Product.objects.filter(stock=0).update(
is_active=False,
updated_at=timezone.now(), # явно
)
Работает только с полями своей модели. update() не поддерживает переход через связи. Нельзя обновить поля связанной модели:
# update() не поддерживает traversal через связи
Product.objects.filter(is_active=True).update(category__name="Updated") # ошибка
update() с F expressions
Для вычислений на основе текущего значения поля (без race condition) используйте F():
from django.db.models import F
# Увеличить stock на 10 для всех продуктов
Product.objects.filter(category__slug="phones").update(stock=F("stock") + 10)
UPDATE shop_product
SET stock = stock + 10
WHERE category_id = (SELECT id FROM shop_category WHERE slug = 'phones');
PostgreSQL выполняет вычисление атомарно, нет race condition. F() подробно разберем в уроке 3.2.
bulk_create() - массовое создание
Создание объектов в цикле через create(), это N запросов для N объектов:
# Плохо - 100 INSERT запросов
for i in range(100):
Product.objects.create(name=f"Product {i}", price=Decimal("10.00"), ...)
bulk_create() отправляет один INSERT с несколькими строками:
products = [
Product(name=f"Product {i}", price=Decimal("10.00"), category=category, slug=f"product-{i}")
for i in range(100)
]
Product.objects.bulk_create(products)
INSERT INTO shop_product (name, price, category_id, slug, ...)
VALUES
('Product 0', 10.00, 1, 'product-0', ...),
('Product 1', 10.00, 1, 'product-1', ...),
-- ... все 100 строк в одном запросе
('Product 99', 10.00, 1, 'product-99', ...);
Один запрос вместо 100. Разница в производительности на реальных данных:
100 объектов через create() в цикле: ~0.5 сек (100 round-trips к БД)
100 объектов через bulk_create(): ~0.01 сек (1 round-trip)
Параметр batch_size
По умолчанию bulk_create() отправляет все объекты в одном запросе. Для очень больших наборов это может исчерпать память или превысить лимит PostgreSQL:
# Разбить на батчи по 500 строк
Product.objects.bulk_create(products, batch_size=500)
Django сам разобьет список на группы и отправит несколько запросов.
Возврат pk и update_conflicts
До Django 4.1 bulk_create() не возвращал pk созданных объектов в некоторых БД. Сейчас PostgreSQL возвращает pk через RETURNING:
created = Product.objects.bulk_create(products)
print(created[0].pk) # pk заполнен
update_conflicts (добавлен в Django 4.1) позволяет обновить существующие строки при конфликте уникального ключа:
Product.objects.bulk_create(
products,
update_conflicts=True,
update_fields=["price", "stock"],
unique_fields=["slug"], # по какому полю определять конфликт
)
INSERT INTO shop_product (slug, name, price, stock, ...)
VALUES (...)
ON CONFLICT (slug) DO UPDATE SET price = EXCLUDED.price, stock = EXCLUDED.stock;
Начиная с Django 5.0, при update_conflicts=True pk возвращается и для обновленных строк.
Ограничения bulk_create()
- Не вызывает сигналы
pre_save/post_save - Не вызывает
save()модели - Не обновляет
auto_nowполя -updated_atнужно устанавливать вручную передbulk_create() - M2M связи - нельзя задать через
bulk_create(), нужно добавлять отдельно
bulk_update() - массовое обновление
Обновление объектов в цикле через save(), снова N запросов:
# Плохо - 1 SELECT + N UPDATE
products = Product.objects.filter(category__slug="phones")
for p in products:
p.price = p.price * Decimal("0.9") # скидка 10%
p.save()
bulk_update() отправляет один UPDATE или несколько батчей:
products = list(Product.objects.filter(category__slug="phones"))
for p in products:
p.price = p.price * Decimal("0.9")
Product.objects.bulk_update(products, fields=["price"])
UPDATE shop_product
SET price = CASE
WHEN id = 1 THEN 899.99
WHEN id = 2 THEN 809.99
WHEN id = 3 THEN 629.99
-- ...
END
WHERE id IN (1, 2, 3, ...);
Django генерирует CASE WHEN внутри одного UPDATE. Для PostgreSQL это значительно быстрее N отдельных UPDATE.
Важно: bulk_update() требует чтобы объекты уже существовали в БД (имели pk). Он не создает новые объекты.
Обязательно указывайте fields, список полей для обновления. Без этого параметра вызов невозможен.
# batch_size, разбить на несколько запросов при большом количестве объектов
Product.objects.bulk_update(products, fields=["price", "stock"], batch_size=500)
Когда bulk_update() неэффективен
Если обновляемое значение одинаково для всех строк: QuerySet.update() лучше:
# Все продукты получают одинаковую скидку, один простой UPDATE
Product.objects.filter(category__slug="phones").update(
price=F("price") * Decimal("0.9")
)
# bulk_update() здесь избыточен, он для разных значений у разных объектов
Удаление через delete()
delete() на объекте
order = Order.objects.get(id=1)
order.delete()
DELETE FROM shop_order WHERE id = 1;
Но если у объекта есть связанные объекты с on_delete=CASCADE, Django сначала найдет их через SELECT, потом удалит.
delete() на QuerySet
# Удалить все отмененные заказы
deleted_count, deleted_detail = Order.objects.filter(status="cancelled").delete()
print(deleted_count) # общее количество удаленных строк
print(deleted_detail) # {"shop.Order": 5, "shop.OrderItem": 23}
-- Django сначала выбирает ids
SELECT id FROM shop_order WHERE status = 'cancelled';
-- Затем удаляет связанные объекты (OrderItem.order on_delete=CASCADE)
DELETE FROM shop_orderitem WHERE order_id IN (1, 2, 3, ...);
-- Затем удаляет сами заказы
DELETE FROM shop_order WHERE id IN (1, 2, 3, ...);
on_delete: как Django управляет удалением
on_delete определяет что происходит со связанными объектами при удалении. В нашем проекте:
OrderItem.order→CASCADE: удаление заказа удаляет его позицииReview.product→CASCADE: удаление продукта удаляет его отзывыOrderItem.product→PROTECT: удалить продукт, у которого есть заказы, нельзя, Django выброситProtectedErrorCategory.parent→SET_NULL: удаление родительской категории обнуляетparentу дочерних
Каскадное удаление работает, но реализовано через Python, а не через базу данных. Django обходит цепочку зависимостей через Collector, делает SELECT для поиска связанных объектов и генерирует DELETE для каждой связанной таблицы - это несколько запросов, как видно в SQL выше.
Убедиться можно через psql - ON DELETE CASCADE в схеме не будет:
docker compose exec db psql -U postgres -d ormcourse -c "\d shop_orderitem"
FOREIGN KEY (order_id) REFERENCES shop_order(id) DEFERRABLE INITIALLY DEFERRED
Django намеренно не делегирует это базе, чтобы иметь возможность вызывать сигналы и применять все варианты on_delete через Python.
Мягкое удаление (soft delete)
Иногда данные нельзя удалять физически, нужна история. Тогда вместо DELETE используют пометку:
# Вместо product.delete()
product.is_active = False
product.save(update_fields=["is_active", "updated_at"])
# Или через update()
Product.objects.filter(id=1).update(is_active=False)
Паттерн soft delete с кастомным менеджером разберем в уроке 8.
Сравнение подходов
Обновление
| Метод | Запросов | Сигналы | Когда использовать |
|---|---|---|---|
save() |
2 (SELECT + UPDATE) | Да | Одиночный объект, нужна бизнес-логика |
save(update_fields=[...]) |
2 (SELECT + UPDATE) | Да | Одиночный объект, частичное обновление |
QuerySet.update() |
1 (UPDATE) | Нет | Массовое обновление одинаковым значением |
bulk_update() |
1-N (CASE WHEN) | Нет | Разные значения для разных объектов |
Создание
| Метод | Запросов | Сигналы | Когда использовать |
|---|---|---|---|
create() |
1 | Да | Одиночный объект |
bulk_create() |
1-N | Нет | Много объектов сразу |
Практическое задание
-
Деактивируйте все продукты с нулевым остатком (
stock=0) черезQuerySet.update(). Посмотрите SQL и количество обновленных строк. -
Создайте 50 тестовых продуктов через
bulk_create(). Замерь время и сравните с созданием в цикле черезcreate(). Для замера используйте:
import time
start = time.time()
# ... операция ...
print(f"{time.time() - start:.3f}s")
-
Получите все продукты категории "books", прибавьте к каждой цене 5 и сохраните через
bulk_update(). Посмотрите SQL, найдите CASE WHEN. -
Удалите один продукт через
product.delete(). Посмотрите в логе SQL сколько запросов выполнилось, есть ли SELECT для поиска связанных объектов?. -
Удалите группу продуктов через
Product.objects.filter(...).delete(). Сравните количество запросов с удалением одного объекта.
Возможные ошибки
save() в цикле вместо bulk_create()
# 1000 запросов
for data in large_dataset:
Product.objects.create(**data)
# 1 запрос или несколько батчей
Product.objects.bulk_create([Product(**data) for data in large_dataset], batch_size=500)
Игнорирование того, что update() не обновляет auto_now
# updated_at НЕ обновится автоматически
Product.objects.filter(stock=0).update(is_active=False)
# Правильно
Product.objects.filter(stock=0).update(
is_active=False,
updated_at=timezone.now(),
)
Использование bulk_update() когда подходит update()
# Избыточно если все объекты получают одно значение
products = list(Product.objects.filter(category__slug="phones"))
for p in products:
p.is_active = False
Product.objects.bulk_update(products, ["is_active"])
# Правильно когда один простой UPDATE
Product.objects.filter(category__slug="phones").update(is_active=False)
delete() без фильтра, удаление всей таблицы
# Удаляет ВСЕ продукты без предупреждения
Product.objects.all().delete()
# Product.objects.delete() # AttributeError: Manager не имеет метода delete()
# Всегда проверяй что в QuerySet перед delete()
qs = Product.objects.filter(is_active=False)
print(f"Будет удалено: {qs.count()}")
qs.delete()
Связь со следующим уроком
Блок 2 завершен, базовые операции CRUD покрыты полностью. В уроке 3.1 начинаем блок продвинутых запросов с Q objects: как строить сложные условия с OR и NOT, как динамически конструировать фильтры и почему exclude() с несколькими условиями может удивить.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru