Введение
Каждый день в проекте появляется небольшая, но полезная идея: быстрый фильтр, кеш‑обёртка, автоматический чек‑интегратор. Собрать их в «дневник» помогает быстро находить решения, а новым членам команды – не «выжигать мозги» по поиску. В статье я собрал проверенные 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‑диаграмма модели
3.2. Поток запросов (middleware → view → ORM → 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.