IT-журнал

Дневник Django‑разработчика: фичи, лайфхаки и ежедневные улучшения

Практический набор фич, улучшений и лайфхаков, проверенных в реальном проекте Django. Пошаговые примеры кода, схемы и типичные подводные камни.

Обновлено 21.09.2026

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

Введение

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

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

Мы работаем над небольшим сервисом DailyNotes – пользователи могут создавать, редактировать и делиться текстовыми заметками. Стек:

Технология Версия
Python 3.11
Django 4.2
Django REST Framework 3.14
PostgreSQL 15

Ограничения проекта:
Ожидаемый трафик – до 200 RPS, запросы читают/пишут в одну таблицу Note.
Требуется быстрый поиск по заголовку и содержимому.
* На проде ограничены миграции – они должны проходить без остановки.

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

2.1. Модели и миграции

Создаём базовую модель с индексацией и UUIDField для публичных ссылок.

# notes/models.py
import uuid
from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model()

class Note(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notes')
    title = models.CharField(max_length=255, db_index=True)
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        indexes = [
            models.Index(fields=['title', 'body'], name='note_fulltext_idx'),
        ]
        ordering = ['-created_at']

    def __str__(self):
        return self.title

Лайфхак: db_index=True сразу создает B‑tree‑индекс. Для полнотекстового поиска в PostgreSQL лучше добавить GIN‑индекс через миграцию RunSQL.

# Создание миграций
python manage.py makemigrations notes
python manage.py migrate

2.2. Сериализаторы и вьюхи

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

class NoteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Note
        fields = ('id', 'author', 'title', 'body', 'created_at', 'updated_at')
        read_only_fields = ('id', 'author', 'created_at', 'updated_at')
# notes/views.py
from rest_framework import viewsets, permissions, filters
from .models import Note
from .serializers import NoteSerializer

class NoteViewSet(viewsets.ModelViewSet):
    """
    CRUD для заметок. Текущий пользователь видит только свои записи.
    """
    queryset = Note.objects.all()
    serializer_class = NoteSerializer
    permission_classes = [permissions.IsAuthenticated]
    filter_backends = [filters.SearchFilter, filters.OrderingFilter]
    search_fields = ['title', 'body']
    ordering_fields = ['created_at', 'updated_at']

    def get_queryset(self):
        # N+1‑catch: сразу подтягиваем автора.
        return Note.objects.select_related('author').filter(author=self.request.user)

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

2.3. URL‑маршруты

# notes/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import NoteViewSet

router = DefaultRouter()
router.register(r'notes', NoteViewSet, basename='note')

urlpatterns = [
    path('api/', include(router.urls)),
]

2.4. Асинхронный кеш‑слой (async‑view)

Django 4.2 поддерживает асинхронные представления. Для часто читаемых списков используем async_to_sync + cache.

# notes/views.py (добавляем)
from django.core.cache import cache
from asgiref.sync import sync_to_async

class AsyncNoteListView(viewsets.ReadOnlyModelViewSet):
    serializer_class = NoteSerializer
    permission_classes = [permissions.IsAuthenticated]

    async def list(self, request, *args, **kwargs):
        cache_key = f'notes:{request.user.id}'
        cached = await sync_to_async(cache.get)(cache_key)
        if cached:
            return Response(cached)

        qs = await sync_to_async(lambda: Note.objects.select_related('author')
                                 .filter(author=request.user).order_by('-created_at'))()
        data = self.get_serializer(qs, many=True).data
        await sync_to_async(cache.set)(cache_key, data, timeout=60)  # 1 минута
        return Response(data)

Лайфхак: в list используем sync_to_async лишь для DB‑запроса, а не для сериализации, чтобы не блокировать цикл событий.

3. Примеры кода и схемы

3.1. ER‑диаграмма модели

erDiagram USER ||--o{ NOTE : author NOTE { uuid id PK varchar title text body datetime created_at datetime updated_at }

3.2. Поток запросов (middleware → view → ORM → response)

sequenceDiagram participant Client participant Middleware participant View participant ORM participant DB participant Cache Client->>Middleware: GET /api/notes/ Middleware->>View: dispatch() View->>Cache: GET notes:user_id alt cache miss View->>ORM: SELECT * FROM notes WHERE author_id=? ORM->>DB: query DB-->>ORM: rows ORM-->>View: queryset View->>Cache: SET notes:user_id end View-->>Client: JSON response

3.3. Сравнительная таблица: обычный list vs async‑кеш

Фактор Обычный list (sync) AsyncNoteListView (async+cache)
Блокирующий I/O Да (DB) Нет (await) + кеш без блокировки
Время отклика (мсек) ~120 ~45 (при кеш‑хите)
Нагрузка на DB 1 запрос/запрос 0 запрос при кеш‑хите
Требования к infra обычный WSGI ASGI + Redis/Memcached

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

Тема Что может сломаться Как избежать
N+1 запросов serializer без select_related тянет автора по отдельности Добавить select_related('author') в get_queryset
Транзакции Несогласованные изменения при одновременных PATCH‑запросах Обернуть perform_update в transaction.atomic()
Индексы Большой объём body без полнотекстового индекса → медленно Добавить GIN‑индекс через RunSQL миграцию
Кеш‑инвалидация Обновление Note не очищает кеш → старые данные В signals.post_save и post_delete сбрасывать кеш
Async‑view Использование синхронных ORM‑запросов в async‑контексте → блокировка Оборачивать ORM‑вызовы в sync_to_async (как выше)
Бэкапы Прямое изменение схемы без миграций приводит к рассинхрону Всегда генерировать миграцию, проверять на тестовом стенде

Сигналы для инвалидации кеша

# notes/signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache
from .models import Note

@receiver([post_save, post_delete], sender=Note)
def invalidate_notes_cache(sender, instance, **kwargs):
    cache_key = f'notes:{instance.author_id}'
    cache.delete(cache_key)

Не забудьте импортировать в apps.py.

# notes/apps.py
from django.apps import AppConfig

class NotesConfig(AppConfig):
    name = 'notes'

    def ready(self):
        import notes.signals

Альтернативные подходы

Задача Стандартный подход Альтернатива
Поиск по тексту SearchFilter + B‑tree PostgreSQL GIN + django.contrib.postgres.search
Кеширование списка cache.set вручную django-cacheops (автоматический)
Асинхронные запросы sync_to_async django‑async‑orm (экспериментально)

5. Вывод

  • Индексы и полнотекстовый поиск дают самые ощутимые улучшения скорости – не забывайте их ставить сразу.
  • select_related избавляет от типичного N+1, а prefetch_related полезен для M2M‑отношений.
  • Кеш + async‑view сокращает время отклика до десятков миллисекунд, но требует ASGI‑стека и отдельного бекенда кеша.
  • Сигналы – простой способ держать кеш в актуальном состоянии, однако в крупных проектах лучше вынести в отдельный task‑queue (Celery).
  • Транзакции следует явно указывать там, где несколько записей меняются атомарно – иначе в случае ошибки часть данных останется неконсистентной.

Что улучшить дальше: перейти на django‑search с TrigramSimilarity для «fuzzy»‑поиска, добавить DRF‑throttling для защиты от спама и автоматизировать инвалидацию кеша через django‑signals‑redis.