📖 Теория — Урок 36
⚡ Ключевые концепции урока
- is_deleted — булево поле для пометки «удалённых» записей без физического DELETE
- SoftDeleteManager — кастомный менеджер, автоматически исключает
is_deleted=True - Ленивая загрузка — QuerySet не делает SQL до реального обращения к данным
- N+1 проблема — цикл по QuerySet с обращением к связанным объектам = N+1 запросов
- select_related() — JOIN для FK/OneToOne, 1 запрос
- prefetch_related() — отдельный запрос для M2M и обратных FK, 2 запроса
- transaction.atomic — атомарный блок: ошибка → откат всех операций
- transaction.on_commit — выполнить callback только после успешного COMMIT
Часть 1: Мягкое удаление (Soft Deletion)
Мягкое удаление (soft deletion) — подход, при котором записи не удаляются физически из базы данных, а помечаются как удалённые. Это позволяет восстанавливать удалённые записи и сохранять историю данных. Мягкое удаление полезно в приложениях, где важно сохранять данные для аудита, исторических записей или восстановления.
Основные концепции мягкого удаления
| Концепция | Описание |
|---|---|
is_deleted |
Булево поле модели — указывает, удалена ли запись |
deleted_at |
DateTimeField — когда была «удалена» запись (для аудита) |
| Фильтрация | Все операции чтения должны исключать записи с is_deleted=True |
Переопределение delete() |
Метод модели устанавливает is_deleted=True вместо физического DELETE |
| Менеджер модели | Класс, управляющий QuerySet — автоматически исключает удалённые записи |
Шаг 1: Добавление поля is_deleted к модели
Добавляем булево поле is_deleted к модели. Не забудьте создать и применить миграцию.
# models.py
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
published_date = models.DateField()
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE,
null=True, blank=True)
created_at = models.DateTimeField(null=True, blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2,
null=True, blank=True)
discounted_price = models.DecimalField(max_digits=10, decimal_places=2,
null=True, blank=True)
is_bestseller = models.BooleanField(default=False)
genres = models.ManyToManyField(Genre, related_name='books')
is_banned = models.BooleanField(default=False)
is_deleted = models.BooleanField(default=False) # поле для мягкого удаления
Шаг 2: Переопределение метода delete()
Переопределяем метод delete() модели: вместо физического удаления устанавливаем флаг.
# models.py
class Book(models.Model):
...
is_deleted = models.BooleanField(default=False)
def delete(self, *args, **kwargs):
self.is_deleted = True
self.save()
delete() — стандартный ORM-метод модели. Переопределение работает одинаково во всех современных версиях Django.
Шаг 3: Кастомный менеджер модели
Менеджер модели — класс, управляющий запросами к модели. Менеджеры предоставляют интерфейс для взаимодействия с базой данных. По умолчанию каждая модель имеет менеджер objects, который позволяет выполнять запросы all(), filter(), get() и т.д.
Основные функции менеджера:
- Создание и фильтрация запросов: менеджеры определяют поведение запросов к модели
- Расширение функциональности: менеджеры позволяют добавлять кастомные методы для специфичных задач
Создаём кастомного менеджера в отдельном файле managers.py:
# managers.py
from django.db import models
class SoftDeleteManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_deleted=False)
Используем менеджер в модели:
# models.py
from .managers import SoftDeleteManager
class Book(models.Model):
...
is_deleted = models.BooleanField(default=False)
objects = SoftDeleteManager() # менеджер для исключения удалённых записей
def delete(self, *args, **kwargs):
self.is_deleted = True
self.save()
Теперь при удалении записи она будет помечена как удалённая с помощью флага is_deleted вместо физического удаления из базы данных. Все стандартные запросы через Book.objects.all() и Book.objects.filter(...) автоматически будут исключать «удалённые» записи.
all_objects = models.Manager() — для административных целей.
Часть 2: Ленивая загрузка в Django ORM
В Django ORM запросы к базе данных выполняются «лениво» (lazy): фактический запрос к базе данных не выполняется до тех пор, пока данные действительно не понадобятся. Это поведение позволяет оптимизировать производительность, избегая ненужных запросов, но может также привести к неожиданным проблемам.
Особенности ленивых запросов
- Создание QuerySet само по себе не вызывает запрос к базе данных
- Методы, возвращающие QuerySet (
filter(),exclude(),all()), — ленивые - Запрос выполняется только при реальном доступе к данным: итерация,
len(),list(),[0],count()
Примеры ленивых запросов
# Запустите Django shell
python manage.py shell
# Создание QuerySet — SQL НЕ выполняется
books = Book.objects.all()
# Запрос выполняется только при итерации
for book in books:
print(book)
# Фильтрация создаёт ленивый QuerySet
filtered_books = Book.objects.filter(author="F. Scott Fitzgerald")
# SQL выполняется только при обращении к данным
print(filtered_books.count())
Проблемы ленивых запросов: N+1
# ПЛОХО: проблема N+1
books = Book.objects.all() # 1 запрос
for book in books:
print(book.publisher) # +1 запрос для каждой книги!
# Итого: 1 + N запросов
Как избежать N+1: select_related() и prefetch_related()
| Метод | Тип отношения | Механизм | Количество запросов |
|---|---|---|---|
select_related() |
ForeignKey, OneToOneField | SQL JOIN | 1 запрос |
prefetch_related() |
ManyToManyField, обратные FK | Отдельный SQL + объединение в Python | 2 запроса |
Использование select_related()
# ХОРОШО: select_related для ForeignKey
books = Book.objects.select_related('publisher').all() # 1 запрос (JOIN)
# Доступ к publisher не вызывает дополнительных запросов
for book in books:
print(book.publisher)
Использование prefetch_related()
# ХОРОШО: prefetch_related для ManyToManyField
books = Book.objects.prefetch_related('genres').all() # 2 запроса
# Доступ к genres не вызывает дополнительных запросов
for book in books:
print("Book:", book, "-> Genres:")
for genre in book.genres.all():
print("\t", genre)
Совместное использование
# ОПТИМАЛЬНО: select_related + prefetch_related вместе
books = Book.objects.select_related('publisher').prefetch_related('genres').all()
for book in books:
print("Publisher:", book.publisher, "->", end=' ')
print("Book:", book, "-> Genres:")
for genre in book.genres.all():
print("\t", genre)
Часть 3: Транзакции
Транзакции в базах данных позволяют группировать несколько операций в одну логическую единицу работы, обеспечивая атомарность и консистентность данных. Если какая-либо часть транзакции не может быть завершена, вся транзакция откатывается, предотвращая частичное обновление данных.
Принципы ACID
| Принцип | Описание |
|---|---|
| Атомарность (Atomicity) | Все операции внутри транзакции выполняются полностью или не выполняются вообще |
| Консистентность (Consistency) | База данных переходит из одного допустимого состояния в другое |
| Изолированность (Isolation) | Транзакции выполняются независимо друг от друга |
| Долговечность (Durability) | Результаты завершённой транзакции сохраняются даже при сбоях системы |
Автоматическое управление транзакциями
По умолчанию Django автоматически управляет транзакциями, фиксируя изменения после каждой операции ORM. Это означает: если несколько операций выполняются подряд и одна вызывает ошибку — только эта операция не будет завершена, предыдущие сохранятся.
# views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Book, Publisher
from .serializers import BookSerializer
@api_view(['POST'])
def create_book_with_publisher(request):
try:
# Каждый create() — отдельная автоматическая транзакция
publisher = Publisher.objects.create(
name="New Publisher",
established_date="2024-06-01"
)
book = Book.objects.create(
title="The Great Gatsby",
author="F. Scott Fitzgerald",
published_date="1925-04-10",
publisher=publisher
)
serializer = BookSerializer(book)
return Response(serializer.data)
except Exception as e:
return Response({'error': str(e)}, status=400)
Publisher создан, а создание Book упало с ошибкой — Publisher останется в базе без Book. Для связанных операций нужны явные транзакции.
Явное управление: @transaction.atomic (декоратор)
@transaction.atomic — декоратор, который обеспечивает атомарность всей функции. При возникновении ошибки все операции внутри функции будут отменены.
# views.py
from django.db import transaction
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Book, Publisher
@transaction.atomic # ← порядок важен: atomic — внешний декоратор
@api_view(['POST'])
def create_book_with_publisher(request):
try:
publisher = Publisher.objects.create(
name="New Publisher",
established_date="2024-06-01"
)
book = Book.objects.create(
title="The Great Gatsby",
author="F. Scott Fitzgerald",
published_date="1925-04-10",
publisher=publisher
)
serializer = BookSerializer(book)
return Response(serializer.data)
except Exception as e:
return Response({'error': str(e)}, status=400)
Явное управление: with transaction.atomic() (контекстный менеджер)
Контекстный менеджер позволяет выделить конкретный блок кода для транзакции, а не всю функцию.
# views.py
from django.db import transaction
@api_view(['POST'])
def create_book_with_publisher(request):
try:
with transaction.atomic():
publisher = Publisher.objects.create(
name="New Publisher",
established_date="2024-06-01"
)
book = Book.objects.create(
title="The Great Gatsby",
author="F. Scott Fitzgerald",
published_date="1925-04-10",
publisher=publisher
)
serializer = BookSerializer(book)
return Response(serializer.data)
except Exception as e:
return Response({'error': str(e)}, status=400)
transaction.set_rollback() — явный откат
Позволяет явно пометить транзакцию для отката в конце блока atomic, даже если исключения не возникло.
# views.py
@api_view(['POST'])
def create_book_with_publisher(request):
try:
with transaction.atomic():
publisher = Publisher.objects.create(name="New Publisher",
established_date="2024-06-01")
book = Book.objects.create(title="New Book", author="New Author",
published_date="2024-06-02",
publisher=publisher)
# Условие для отката: если publisher с таким именем уже существует
if Publisher.objects.filter(name="New Publisher").count() > 1:
transaction.set_rollback(True)
serializer = BookSerializer(book)
return Response(serializer.data)
except Exception as e:
return Response({'error': str(e)}, status=400)
transaction.on_commit() — код после успешного коммита
Регистрирует функцию, которая будет выполнена только после успешной фиксации транзакции. Типичное применение: отправка email, вызов Celery-задачи, отправка уведомлений.
# views.py
from django.db import transaction
@api_view(['POST'])
def create_book_with_publisher(request):
def notify_success(book):
print(f"\n{'—' * 25} Book '{book.title}' created successfully! {'—' * 25}\n")
try:
with transaction.atomic():
publisher = Publisher.objects.create(name="New Publisher",
established_date="2024-06-01")
book = Book.objects.create(title="New Book", author="New Author",
published_date="2024-06-02",
publisher=publisher)
# Callback выполнится только после COMMIT
transaction.on_commit(lambda: notify_success(book))
serializer = BookSerializer(book)
return Response(serializer.data)
except Exception as e:
return Response({'error': str(e)}, status=400)
Ручное управление транзакциями
В редких случаях может потребоваться тонкий контроль через commit() и rollback(). Это низкоуровневый подход; предпочтительнее использовать transaction.atomic.
# views.py
from django.db import transaction
@api_view(['POST'])
def create_book_with_publisher(request):
try:
transaction.set_autocommit(False) # отключить автокоммит
publisher = Publisher.objects.create(name="New Publisher",
established_date="2024-06-01")
book = Book.objects.create(title="The Great Gatsby",
author="F. Scott Fitzgerald",
published_date="1925-04-10",
publisher=publisher)
transaction.commit() # явный коммит
serializer = BookSerializer(book)
return Response(serializer.data)
except Exception as e:
transaction.rollback() # откат при ошибке
return Response({'error': str(e)}, status=400)
finally:
transaction.set_autocommit(True) # вернуть автокоммит
Когда что использовать
| Ситуация | Рекомендация |
|---|---|
| Одна операция ORM | Автоматическое управление (по умолчанию) |
| Несколько связанных операций | with transaction.atomic() |
| Вся функция должна быть атомарной | @transaction.atomic |
| Код после успешного коммита (email, очередь) | transaction.on_commit() |
| Условный откат без исключения | transaction.set_rollback(True) |