Почему я перестал использовать ORM в новых проектах
Первый проект на ORM я поднял в 2019-м и был доволен: модели описаны один раз, миграции едут сами, код читается. Проблемы начались, когда запросов стало больше пятнадцати на экран и половина из них перестала помещаться в простые фильтры.
Что произошло
Сложную выборку всё равно приходится писать руками. То нужны оконные функции, то CTE, то специфичный join с условием, которое ORM выражает настолько громоздко, что проще спуститься на уровень SQL. В итоге в проекте сосуществуют два стиля: простые операции через ORM и всё интересное через сырые запросы. Поддерживать это труднее, чем один последовательный подход.
Вторая проблема — отладка. Когда запрос тормозит, нужно понимать, какой SQL
ушёл в базу. С ORM это лишний шаг: включи логирование, найди сгенерированный текст,
сопоставь его с кодом на Python или Go. С рукописным SQL текст запроса лежит прямо
в коде, и EXPLAIN ANALYZE применяется к нему без перевода.
ORM экономит время на прототипе и стоит времени на поддержке. Вопрос только в том, какой этап для вас длиннее.
Что делаю теперь
- Простой CRUD и прототипы — ORM остаётся, она здесь действительно быстрее.
- Боевые сервисы — сырые запросы плюс тонкая обёртка для сканирования строк.
- Миграции — отдельный инструмент, не привязанный к маппингу объектов.
- Каждый запрос в коде сопровождается комментарием, зачем он нужен и какой индекс использует.
На Go для этого хорошо подходит sqlx или pgx напрямую,
на Python — asyncpg без прослойки. Код получается длиннее, но каждая
строка в нём означает ровно то, что произойдёт в базе.
Пример
Выборка последних событий по каждому пользователю. Через ORM это попытка выразить оконную функцию средствами QuerySet. Напрямую — один читаемый запрос:
SELECT DISTINCT ON (user_id) user_id, created_at, event_type
FROM events
WHERE created_at > now() - interval '7 days'
ORDER BY user_id, created_at DESC;
Плюс частичный индекс (user_id, created_at DESC) с условием
по дате — и запрос идёт за миллисекунды на миллионах строк.