Создание объектов Django ORM | Курс по Django ORM урок 2.1
Цель урока
Разобрать все способы создания объектов в Django ORM: save(), create(), get_or_create(), update_or_create(). Понять, чем они отличаются на уровне SQL, сколько запросов каждый делает и в каких ситуациях что выбирать.
Необходимые знания
- Уроки 1.1 - 1.4: модели, поля, миграции
- Базовое понимание INSERT в SQL
save() - базовый способ
Самый явный способ создать объект, создать экземпляр модели и вызвать save().
from shop.models import Category, Product
from decimal import Decimal
# Создаем объект в памяти, никаких запросов к БД
category = Category.objects.get(slug="phones")
product = Product(
name="OnePlus 12",
slug="oneplus-12",
price=Decimal("699.99"),
stock=50,
category=category,
)
# Только здесь идет запрос к БД
product.save()
SQL при save() на новом объекте:
INSERT INTO shop_product
(name, slug, description, price, stock, is_active, created_at, updated_at, category_id)
VALUES
('OnePlus 12', 'oneplus-12', '', 699.99, 50, true, NOW(), NOW(), 3)
RETURNING id;
Django использует RETURNING id (возможности PostgreSQL), чтобы получить сгенерированный первичный ключ без второго запроса. После save() атрибут product.id содержит реальный id из БД.
save() на существующем объекте - UPDATE
Если объект уже имеет pk, save() делает UPDATE:
product = Product.objects.get(slug="oneplus-12")
product.price = Decimal("649.99")
product.save()
UPDATE shop_product
SET name = 'OnePlus 12',
slug = 'oneplus-12',
description = '',
price = 649.99,
stock = 50,
is_active = true,
updated_at = NOW(),
category_id = 3
WHERE id = 42;
Обратите внимание: Django обновляет все поля, даже те, которые не менялись. Это расточительно если у модели много полей. Чтобы обновить только конкретные поля:
product.price = Decimal("649.99")
product.save(update_fields=["price", "updated_at"])
UPDATE shop_product
SET price = 649.99, updated_at = NOW()
WHERE id = 42;
Если вы знаете что изменили, всегда указывайте update_fields.
Количество запросов
product = Product(name="...", ...) # 0 запросов
product.save() # 1 запрос (INSERT)
create() - создание за один шаг
create(), это Product(**kwargs) + save() в одном вызове:
product = Product.objects.create(
name="OnePlus 12",
slug="oneplus-12",
price=Decimal("699.99"),
stock=50,
category=category,
)
SQL идентичен save(), один INSERT. Разница только в синтаксисе Python.
create() возвращает созданный объект с заполненным pk. Используйте его когда не нужно устанавливать значения атрибутов поэтапно.
Количество запросов
product = Product.objects.create(...) # 1 запрос (INSERT)
get_or_create() - получить или создать
Решает задачу: "получите объект с такими параметрами, если его нет, создайте". Атомарная операция с точки зрения Django, один или два запроса, но без race condition при правильном использовании.
category, created = Category.objects.get_or_create(
slug="smartphones", # поля для поиска (WHERE)
defaults={"name": "Smartphones"}, # поля только для создания
)
if created:
print(f"Создана новая категория: {category.name}")
else:
print(f"Найдена существующая категория: {category.name}")
SQL когда объект существует:
SELECT * FROM shop_category WHERE slug = 'smartphones' LIMIT 1;
-- объект найден, возвращает (category, False)
SQL когда объекта нет:
SELECT * FROM shop_category WHERE slug = 'smartphones' LIMIT 1;
-- не найдено
INSERT INTO shop_category (slug, name, parent_id)
VALUES ('smartphones', 'Smartphones', NULL)
RETURNING id;
-- возвращает (category, True)
Итого 1 или 2 запроса.
defaults - поля только для создания
Параметры в defaults используются только при создании. При поиске они игнорируются.
# Ищем по slug. Если найдем, name из defaults НЕ применяется к найденному объекту.
# Если не найдем, то создаем с name="Smartphones"
category, created = Category.objects.get_or_create(
slug="smartphones",
defaults={"name": "Smartphones"},
)
Если нужно и создать, и обновить, используйте update_or_create.
Race condition в get_or_create
get_or_create не полностью защищен от гонки в высоконагруженных системах. Два процесса одновременно могут не найти объект и оба попытаются сделать INSERT. Первый успеет, второй получит ошибку уникальности.
Django обрабатывает это при IntegrityError во время INSERT он делает повторный SELECT. Но это работает только если поля в поиске покрыты unique или unique_together. Убедитесь, что поля поиска имеют уникальное ограничение в БД.
# slug - unique, поэтому get_or_create безопасен
Category.objects.get_or_create(slug="smartphones", defaults={...})
# name не unique, get_or_create может создать дубли или упасть
Category.objects.get_or_create(name="Smartphones", defaults={...}) # опасно
Количество запросов
obj, created = Model.objects.get_or_create(...)
# Если объект есть: 1 SELECT
# Если объекта нет: 1 SELECT + 1 INSERT
update_or_create() - обновить или создать
Работает как get_or_create, но если объект найден, обновляет его поля из defaults.
from decimal import Decimal
product, created = Product.objects.update_or_create(
slug="oneplus-12", # поля для поиска
defaults={ # поля для создания ИЛИ обновления
"name": "OnePlus 12",
"price": Decimal("699.99"),
"stock": 50,
"category": category,
},
)
SQL когда объект существует:
SELECT * FROM shop_product WHERE slug = 'oneplus-12' LIMIT 1;
UPDATE shop_product
SET name = 'OnePlus 12', price = 699.99, stock = 50, updated_at = NOW()
WHERE id = 42;
SQL когда объекта нет:
SELECT * FROM shop_product WHERE slug = 'oneplus-12' LIMIT 1;
INSERT INTO shop_product (slug, name, price, stock, ...) VALUES (...) RETURNING id;
Итого: 1 SELECT + 1 UPDATE или 1 SELECT + 1 INSERT = всегда 2 запроса.
create_defaults - разные данные для создания и обновления (Django 5.0+)
До Django 5.0 нельзя было задать разные значения для создания и обновления. Django 5.0 добавил create_defaults:
product, created = Product.objects.update_or_create(
slug="oneplus-12",
defaults={ # применяется при UPDATE
"price": Decimal("699.99"),
"stock": 50,
},
create_defaults={ # применяется при INSERT (объединяется с defaults)
"name": "OnePlus 12",
"category": category,
"description": "Flagship smartphone",
},
)
При создании используются defaults + create_defaults. При обновлении, только defaults. Это полезно когда некоторые поля нужно задать при создании, но не перезаписывать при обновлении.
Model.NotUpdated (Django 6.0+)
В Django 6.0 появилось исключение Model.NotUpdated, поднимается когда save(force_update=True) не нашел строку для обновления (затронуто 0 строк). Раньше возникал DatabaseError.
try:
product.save(force_update=True)
except Product.NotUpdated:
# объект был удален параллельным процессом
handle_missing_product()
Количество запросов
obj, created = Model.objects.update_or_create(...)
# Всегда: 1 SELECT + 1 INSERT или 1 SELECT + 1 UPDATE = 2 запроса
Сравнение методов
| Метод | Запросов | Когда использовать |
|---|---|---|
Model() + save() |
1 (INSERT) | Когда нужно поэтапно заполнять поля |
create() |
1 (INSERT) | Создать объект за один вызов |
get_or_create() |
1-2 | Идемпотентное создание, поле - unique |
update_or_create() |
2 | Синхронизация данных из внешнего источника |
Сигналы и save()
save() и create() вызывают сигналы pre_save и post_save. Это важно если в проекте есть обработчики сигналов, они выполнятся при каждом save().
Если есть такой обработчик:
@receiver(post_save, sender=Product)
def update_search_index(sender, instance, created, **kwargs):
if created:
search_index.add(instance)
То Product.objects.create(...) вызовет update_search_index. А bulk_create(...) по умолчанию НЕ вызовет, разберем в уроке 2.5.
Валидация при создании
save() и create() не вызывают full_clean() автоматически. Валидация Django (validators, clean()) не выполняется при прямом сохранении через ORM.
# Это НЕ вызовет ошибку, даже если rating=10 нарушает логику
Review.objects.create(product=product, user=user, rating=10, text="")
# Но CheckConstraint в БД не пропустит и PostgreSQL вернет ошибку
Если нужна валидация на уровне Django, вызывайте явно:
review = Review(product=product, user=user, rating=10)
review.full_clean() # вызовет все валидаторы и clean()
review.save()
Constraints на уровне БД (урок 6.3) работают всегда, независимо от того, через ORM пришли данные или нет.
Практическое задание
-
Создайте новый продукт
Product()+save(). В логировании SQL в консоли убедитесь, что выполнился ровно один INSERT с RETURNING. -
Измените цену созданного продукта и сохраните с помощью
save(update_fields=["price", "updated_at"]). Сравните SQL с обычнымsave(), сколько полей в SET? -
Используйте
get_or_createдля создания категории "Accessories". Вызовите дважды с одинаковыми параметрами. Убедитесь что второй вызов не создает дубль и возвращаетcreated=False. -
Используйте
update_or_createдля синхронизации продукта, если продукт с таким slug существует, обнови цену и stock, если нет, создайте с полным набором полей. Проверьте оба сценария.
Возможные ошибки
save() без update_fields при частичном обновлении
product = Product.objects.get(id=1)
product.price = Decimal("499.99")
product.save()
# Обновляет ВСЕ поля, включая updated_at, name, stock...
# Если параллельный процесс изменил stock, изменение будет потеряно
# Правильно:
product.save(update_fields=["price", "updated_at"])
Поиск в get_or_create по не уникальному полю
# name не уникален и может создать дубли
Category.objects.get_or_create(name="Electronics")
# get() внутри get_or_create вызовет MultipleObjectsReturned если найдет несколько
Игнорирование возвращаемого created
# Теряем информацию, создан объект или найден
category = Category.objects.get_or_create(slug="phones")[0]
# Лучше всегда распаковывать
category, created = Category.objects.get_or_create(slug="phones")
Передача объекта FK туда, где ждут id
# Оба варианта работают, но второй на один атрибут эффективнее
Product.objects.create(category=category) # Django сам возьмет category.id
Product.objects.create(category_id=category.id) # чуть явнее, тот же SQL
Связь со следующим уроком
В уроке 2.2 переходим к чтению данных, QuerySet API. Разберем, что такое QuerySet и почему он ленивый, какие методы возвращают QuerySet, а какие немедленно выполняют запрос. И как values(), only(), defer() влияют на то, какие колонки попадают в SELECT.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru