IT-журнал

Django в повседневной работе: фичи, лайфхаки и улучшения для заметок

Практические фичи, лайфхаки и оптимизации Django для ежедневных заметок: модели, DRF‑эндпоинты, транзакции, индексы и async‑вью.

Обновлено 21.09.2026

⏱ 3 мин чтения · 👁 прочитали 2

Вступление
Каждый день программист пишет небольшие «заметки»: TODO‑списки, прототипы API, идеи по рефакторингу. Обычное приложение «Заметки» в Django позволяет быстро фиксировать мысли и одновременно отточить навыки работы с ORM, DRF и современными возможностями фреймворка. В статье мы построим минимальное, но полностью рабочее CRUD‑приложение для заметок, а затем разберём набор проверенных лайфхаков, которые делают код чище, быстрее и более надёжным.

Что получит читатель: готовое приложение, набор практических приёмов (select_related, bulk_create, transaction.atomic, index_together, async‑views) и список типичных «граблей», которых стоит избегать.


1. Контекст и предпосылки

Параметр Значение
Python 3.11
Django 4.2 (LTS)
Django REST Framework 3.14
PostgreSQL 15
Цель проекта Минимальный сервис для личных заметок, доступный через веб‑интерфейс и JSON‑API.
Ограничения Один пользователь (без аутентификации), но код готов к масштабированию.

Почему именно такие версии?
Django 4.2 впервые поддерживает полностью async‑ORM‑операции (например, aqueryset), а DRF 3.14 уже совместим с async‑views. PostgreSQL 15 даёт возможности GIN‑индексов для полнотекстового поиска, если в дальнейшем понадобится поиск по содержимому заметки.


2. Пошаговая реализация

2.1 Модели

# notes/models.py
from django.db import models
from django.utils import timezone


class Note(models.Model):
    """Простая заметка с поддержкой мягкого удаления."""
    title = models.CharField(max_length=200, db_index=True)
    body = models.TextField()
    created_at = models.DateTimeField(default=timezone.now, db_index=True)
    updated_at = models.DateTimeField(auto_now=True)
    is_deleted = models.BooleanField(default=False, db_index=True)

    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["title"], name="idx_note_title"),
            models.Index(fields=["created_at"], name="idx_note_created"),
        ]

    def soft_delete(self):
        """Не удаляем запись из БД, а только помечаем её."""
        self.is_deleted = True
        self.save(update_fields=["is_deleted", "updated_at"])

    def __str__(self):
        return self.title

Лайфхак: добавляем db_index=True сразу к полям, по которым будем фильтровать (title, created_at, is_deleted). Это избавит от последующего пере‑индексирования.

2.2 Сериализаторы

# notes/serializers.py
from rest_framework import serializers
from .models import Note


class NoteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Note
        fields = ("id", "title", "body", "created_at", "updated_at")
        read_only_fields = ("id", "created_at", "updated_at")

2.3 Вьюхи

2.3.1 Синхронные API‑вьюхи (DRF Generic)

# notes/views.py
from rest_framework import generics, status
from rest_framework.response import Response
from .models import Note
from .serializers import NoteSerializer
from django.db import transaction
from django.db.models import Q


class NoteListCreateView(generics.ListCreateAPIView):
    """GET – список активных заметок, POST – создание новой."""
    serializer_class = NoteSerializer

    def get_queryset(self):
        # Исключаем мягко удалённые записи
        return Note.objects.filter(is_deleted=False)

    @transaction.atomic
    def perform_create(self, serializer):
        # Пример использования transaction.atomic для гарантии целостности
        serializer.save()


class NoteRetrieveUpdateDeleteView(generics.RetrieveUpdateDestroyAPIView):
    """GET/PUT/PATCH/DELETE конкретной заметки."""
    serializer_class = NoteSerializer
    lookup_url_kwarg = "pk"

    def get_queryset(self):
        return Note.objects.filter(is_deleted=False)

    def perform_destroy(self, instance):
        # Мягкое удаление вместо hard‑delete
        instance.soft_delete()

2.3.2 Async‑вьюха для массового импорта

# notes/views.py (async часть)
from django.http import JsonResponse
from django.views import View
from asgiref.sync import sync_to_async
import json


class BulkImportView(View):
    """
    Асинхронный эндпоинт, получающий список заметок в JSON и создающий их
    пакетно через bulk_create. Работает только в ASGI‑режиме.
    """

    async def post(self, request):
        data = json.loads(request.body)
        notes = [
            Note(
                title=item["title"],
                body=item.get("body", ""),
                created_at=item.get("created_at", timezone.now()),
            )
            for item in data
        ]

        # bulk_create внутри sync‑обёртки, т.к. ORM пока sync‑внутри
        await sync_to_async(Note.objects.bulk_create)(notes, batch_size=500)

        return JsonResponse({"created": len(notes)}, status=201)

Почему sync_to_async? Django 4.2 пока не реализует полностью async‑ORM; bulk_create остаётся синхронным, но мы можем вызвать его в отдельном потоке без блокировки event‑loop.

2.4 URL‑маршрутизация

# notes/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("api/notes/", views.NoteListCreateView.as_view(), name="note-list"),
    path("api/notes/<int:pk>/", views.NoteRetrieveUpdateDeleteView.as_view(), name="note-detail"),
    path("api/notes/bulk-import/", views.BulkImportView.as_view(), name="note-bulk-import"),
]

2.5 Миграции

$ python manage.py makemigrations notes
$ python manage.py migrate

Грабль: если после изменения Meta.indexes добавили новый индекс, миграция может занять несколько минут на больших таблицах. Планируйте время простоя или используйте CONCURRENTLY в PostgreSQL (требует отдельного SQL‑скрипта).


3. Примеры кода и практические лайфхаки

3.1 Оптимизация запросов: select_related vs prefetch_related

Для заметок у нас нет внешних ключей, но в реальном проекте часто добавляют author = ForeignKey(User). В этом случае:

Ситуация Выбор
Получаем список заметок с автором Note.objects.select_related('author')
Получаем список заметок + их теги (ManyToMany) Note.objects.prefetch_related('tags')

select_related использует JOIN, а prefetch_related делает отдельный запрос и «привязывает» результаты в Python. Ошибка «N+1 queries» появляется, когда забывают оба метода.

3.2 Транзакционный контроль

from django.db import transaction

def update_multiple_notes(note_ids, new_title):
    with transaction.atomic():
        notes = Note.objects.select_for_update().filter(id__in=note_ids, is_deleted=False)
        notes.update(title=new_title)

Лайфхак: select_for_update() блокирует строки до коммита, избегая гонок при параллельных обновлениях.

3.3 Индексация и ограничения

class Meta:
    constraints = [
        models.UniqueConstraint(fields=["title"], condition=Q(is_deleted=False), name="unique_active_title")
    ]

Это гарантирует, что две активные заметки не могут иметь одинаковый заголовок, но мягко удалённые могут.

3.4 Кеширование часто запрашиваемого списка

# notes/utils.py
from django.core.cache import cache
from .models import Note

def get_recent_notes(limit=10):
    key = f"recent_notes:{limit}"
    notes = cache.get(key)
    if notes is None:
        notes = list(Note.objects.filter(is_deleted=False).order_by("-created_at")[:limit])
        cache.set(key, notes, timeout=60)  # 1 минута
    return notes

Грабль: кэшировать ORM‑объекты безопасно, только если они не изменятся в течение TTL. Иначе используйте сериализацию (например, serializer.data).

3.5 Асинхронные сигналы (Django 4.2)

# notes/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Note
import asyncio


@receiver(post_save, sender=Note)
async def notify_on_new_note(instance, created, **kwargs):
    if created and not instance.is_deleted:
        await asyncio.sleep(0)  # имитация асинхронной отправки
        # например, отправка в Slack через aiohttp (не показано)

Чтобы активировать асинхронный обработчик, добавьте в apps.py:

class NotesConfig(AppConfig):
    name = "notes"

    def ready(self):
        import notes.signals  # noqa

4. Нюансы, грабли и альтернативы

Проблема Как избежать Альтернативный подход
N+1 запросов в списках select_related/prefetch_related Использовать raw SQL (raw()) только в критических участках
Большие миграции индексов CONCURRENTLY (ручной SQL) Планировать в окно обслуживания
Блокировки при массовом обновлении select_for_update + transaction.atomic Пакетировать операции (bulk_update)
Смотрим только активные записи Всегда фильтровать is_deleted=False Реализовать менеджер objects = NoteManager()
Асинхронные ORM‑операции Пока большинство остаются sync, используем sync_to_async Переход на полностью async‑ORM (например, tortoise-orm) только при необходимости

4.1 Менеджер для «только активные» записи

# notes/managers.py
from django.db import models


class ActiveNoteManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_deleted=False)

И в модели:

class Note(models.Model):
    # ... поля ...
    objects = ActiveNoteManager()          # default manager
    all_objects = models.Manager()         # для админки, если нужен доступ к удалённым

4.2 Полнотекстовый поиск (PostgreSQL)

Если понадобится поиск по body, добавляем GIN‑индекс:

class Meta:
    indexes = [
        models.Index(
            name="idx_note_body_gin",
            fields=["body"],
            opclasses=["gin_trgm_ops"],  # require pg_trgm extension
        ),
    ]

Тогда запрос:

Note.objects.filter(body__search="django")

5. Вывод

  • Ключевые тейкэвы
  • Добавляйте индексы сразу, а не «потом».
  • Мягкое удаление + кастомный менеджер упрощают фильтрацию без изменения бизнес‑логики.
  • transaction.atomic + select_for_update защищают от гонок при массовом изменении.
  • Для больших импортов используйте bulk_create в сочетании с sync_to_async.
  • select_related / prefetch_related избавляют от типичной ошибки N+1.

  • Ограничения

  • Полностью асинхронный ORM пока не доступен в официальном Django 4.2.
  • Индексация GIN требует расширения pg_trgm в PostgreSQL.
  • Мягкое удаление усложняет уникальные ограничения, их нужно задавать через UniqueConstraint с условием.

  • Что можно улучшить

  • Перевести менеджеры на QuerySet.as_manager() для более гибкой цепочки вызовов.
  • Добавить Celery‑задачу для асинхронной отправки уведомлений вместо простого asyncio.sleep.
  • Ввести DRF‑throttling и django-axes для защиты публичного API, если приложение переедет из «одного пользователя» в «мульти‑user».

Эти лайфхаки делают простое приложение «Заметки» пригодным для реального проекта, а также показывают, как извлечь максимум из современных возможностей Django. Happy coding!