📖 Теория — Урок 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) — подход, при котором записи не удаляются физически из базы данных, а помечаются как удалённые. Это позволяет восстанавливать удалённые записи и сохранять историю данных. Мягкое удаление полезно в приложениях, где важно сохранять данные для аудита, исторических записей или восстановления.

Когда применять: системы с аудитом (финансы, медицина), корзина/восстановление, историческая трассировка изменений, соблюдение GDPR (право на «забывание» — мягкое удаление с анонимизацией).

Основные концепции мягкого удаления

КонцепцияОписание
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()
Django 5.x: метод 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: при итерации по записям и обращении к связанным объектам Django выполняет по 1 дополнительному запросу на каждую запись. 100 книг = 101 SQL-запрос.
# ПЛОХО: проблема 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)