Django ORM

Обновление и удаление в Django ORM | Курс Django ORM урок 2.5

Обновление и удаление в Django ORM | Курс Django ORM урок 2.5
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 9

Цель урока

Разобрать все способы обновления и удаления данных в Django ORM. Понять разницу между save() и update() на уровне QuerySet, между delete() на объекте и на QuerySet. Изучить bulk_create() и bulk_update(), и увидеть реальную разницу в производительности.

Необходимые знания

  • Урок 2.1: создание объектов, save()
  • Урок 2.2: QuerySet API
  • Базовое понимание UPDATE и DELETE в SQL

Обновление через 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;

Проблемы:

  1. Сначала SELECT (чтобы получить объект), потом UPDATE - 2 запроса
  2. UPDATE обновляет все поля, даже неизменившиеся
  3. 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.orderCASCADE: удаление заказа удаляет его позиции
  • Review.productCASCADE: удаление продукта удаляет его отзывы
  • OrderItem.productPROTECT: удалить продукт, у которого есть заказы, нельзя, Django выбросит ProtectedError
  • Category.parentSET_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 Нет Много объектов сразу

Практическое задание

  1. Деактивируйте все продукты с нулевым остатком (stock=0) через QuerySet.update(). Посмотрите SQL и количество обновленных строк.

  2. Создайте 50 тестовых продуктов через bulk_create(). Замерь время и сравните с созданием в цикле через create(). Для замера используйте:

   import time
   start = time.time()
   # ... операция ...
   print(f"{time.time() - start:.3f}s")
  1. Получите все продукты категории "books", прибавьте к каждой цене 5 и сохраните через bulk_update(). Посмотрите SQL, найдите CASE WHEN.

  2. Удалите один продукт через product.delete(). Посмотрите в логе SQL сколько запросов выполнилось, есть ли SELECT для поиска связанных объектов?.

  3. Удалите группу продуктов через 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() с несколькими условиями может удивить.


<< Урок 2.4

Урок 3.1 >>


Подписывайтесь на мой Telegram канал

Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы