What ORMs have taught me: just learn SQL (2014)
Начинаю работу над постом. Сначала построю план, затем напишу черновик и пройду все проверки качества.
Шаг 1: строю план поста
- Хук — проблема с ORM
- Почему ORM скрывают SQL
- Что теряется без знания SQL
- Когда ORM допустимы
- Практический вывод
Ключевые идеи из статьи для переработки:
- Автор 30 месяцев работал с SQLAlchemy и Hibernate
- «Object/Relational Impedance Mismatch» — реальная проблема
- Partial objects, attribute creep — таблицы с 100+ атрибутами
- Foreign keys создают 14+ joins на один объект
- Window functions невозможно нормально написать в ORM
- Dual schema — определение данных в двух местах
- Transactions — динамическая область видимости
- Автор пришёл к выводу: база данных — это API с запросами, а не хранилище объектов
Шаг 2: пишу черновик
Почему orm — это боль, а SQL — спасение
Знаете, что меня бесит больше всего в современной разработке? Когда junior-разработчик не может написать простой JOIN без ORM. Или когда человек с 5 годами опыта не понимает, почему его запрос выполняется 3 минуты, хотя данных там — кот наплакал.
Всё это — последствия одной и той же болезни: ORM стали заменой SQL, а не дополнением к нему.
Недавно наткнулся на статью 2014 года от Adam Wozniak — инженера, который 30 месяцев проработал с SQLAlchemy и Hibernate. Его вывод простой и жёсткий: ORM приносят больше вреда, чем пользы. И знаете что? Я с ним согласен. Давайте разберёмся почему.
Проблема, которую все игнорируют
ORM (Object-Relational Mapping — объектно-реляционное отображение) обещают простоту: вместо SQL пишешь код на привычном языке, а ORM сама генерирует запросы. Красиво на бумаге. Проблема в том, что это иллюзия.
Wozniak описывает термин из статьи Ted Neward — «Object/Relational Impedance Mismatch». Это академическое название простой вещи: объекты в коде и таблицы в базе — это разные миры. Объекты имеют иерархии, наследование, методы. Таблицы — это строки и столбцы. Попытка «склеить» их всегда приводит к проблемам.
Каким именно? Вот конкретные примеры из практики автора статьи.
Attribute creep: когда таблица становится монстром
Представьте: у вас есть сущность, к которой постоянно добавляют новые поля. Клиент прислал данные — добавили поле. Маркетинг запросил аналитику — ещё поле. Через год у вас таблица с 100+ атрибутами.
В SQL это не проблема. Вы пишете `SELECT id, name, email FROM users WHERE …` и получаете ровно то, что нужно.
В ORM? Разработчик пишет что-то вроде:
query(Foo.class).add(Restriction.eq("x", value))
ORM загружает все 100+ атрибутов. Все. Каждый раз. Wozniak рассказывает, как оптимизировал такой запрос — добавил правильную проекцию (выбор нужных полей) — и время выполнения упало с минут до секунд. Вся задержка была в конвертации строк базы данных в Java-объекты.
ORM "УДОБСТВО" vs РЕАЛЬНОСТЬ
────────────────────────────
ORM: query(Foo.class).eq("x", val)
│
▼
┌──────────────────┐
│ SELECT * FROM │ ← Забираем ВСЁ
│ foo WHERE x=val │ 100+ колонок
└──────────────────┘
│
▼
┌──────────────────┐
│ Преобразование │ ← Медленно!
│ в Java-объект │
└──────────────────┘
│
▼
Итог: минуты вместо секунд
SQL: SELECT id, x FROM foo WHERE x=val
│
▼
┌──────────────────┐
│ Только нужные │
│ данные │
└──────────────────┘
│
▼
Итог: секунды
Foreign keys: скрытые джойны
Ещё одна ловушка — связи между таблицами. В ORM вы описываете отношения классами. Выглядит чисто и понятно. Но за кулисами это превращается в JOIN-ы.
Wozniak приводит впечатляющую цифру: в его проекте одна сущность требовала 14 JOIN-ов для загрузки. 14! Представьте себе запрос, который джойнит 14 таблиц, а вы даже не видите этот SQL.
Если вы не понимаете, что такое JOIN (а многие ORM-разработчики не понимают), вы не сможете оптимизировать такой запрос. Вы даже не поймёте, почему он тормозит.
Window functions: боль высшего порядка
Window functions (оконные функции) — это продвинутый SQL для аналитики: `RANK ()`, `LAG ()`, `SUM () OVER ()`. Они позволяют делать в базе то, что иначе потребовало бы множества запросов и постобработки в коде.
Попробуйте написать оконную функцию через ORM. Спойлер: это боль. Большинство ORM не поддерживают их нативно. В итоге разработчик либо:
- Пишет сырой SQL (и зачем тогда ORM?)
- Забирает все данные в приложение и считает в коде (медленно, много памяти)
- Использует костыли и обходные пути (技术支持)
Wozniak в таких случаях использовал шаблонизатор для SQL, а описания таблиц оставлял в ORM. Компромисс, который говорит сам за себя.
Dual schema: определение данных в двух местах
Вам нужно определить структуру данных. Где она живёт?
- В базе данных? Тогда зачем вам ORM?
- В приложении? Тогда как синхронизировать с базой?
ORM предлагают миграции — автоматическое изменение схемы базы. Звучит здорово, пока не попробуете. Миграции — это отдельный слой сложности. Они работают плохо с большими объёмами данных. Они конфликтуют с продакшеном, где данные уже есть.
Wozniak пришёл к простому принципу: схема данных живёт в базе. Приложение работает с результатами запросов. Запросы — это ваш API к базе.
Transactions: динамическая область видимости
Transactions (транзакции) — это механизм, который гарантирует целостность данных. В SQL они естественны. В ORM — это головная боль.
Транзакции динамически ограничены по времени. Код внутри транзакции «знает» о ней, код снаружи — нет. Это создаёт путаницу: функция работает в одном контексте и не работает в другом. Приходится передавать «сессии» и «контексты» через полприложения.
Wozniak называет это «modularity trick» — вы пишете «полезную функцию», которая работает только в определённых условиях. Звучит как антипаттерн — и он таким и является.
Практический вывод
Что я вынес из этой статьи (и из своего опыта)?
ORM — это инструмент представления данных, а не инструмент написания запросов. Используйте их для описания схемы, для простых CRUD-операций. Но для серьёзной работы с базой — учите SQL.
Конкретно:
- Изучите SQL хотя бы на среднем уровне. JOIN, GROUP BY, подзапросы, оконные функции. Без этого вы слепы.
- Не используйте `SELECT *`. Никогда. Особенно в ORM.
- Смотрите сгенерированный SQL. Все ORM позволяют увидеть итоговый запрос. Смотрите. Анализируйте.
- Думайте о базе как об API. Таблицы — это типы данных. Запросы — это функции. Возвращаемые значения — это объекты.
БАЗА ДАННЫХ КАК API
────────────────────
┌─────────────────────────────────┐
│ База данных │
│ ┌───────────────────────────┐ │
│ │ Таблицы = Типы данных │ │
│ │ Запросы = Функции │ │
│ │ Результат = Объект │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
│
▼
┌─────────────────────────┐
│ Запрос = API вызов │
│ SELECT ... = return │
│ WHERE ... = фильтр │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ Результат в коде │
│ как обычный объект │
└─────────────────────────┘
Когда orm ещё допустимы
Буду честен: не всё так однозначно. ORM имеют смысл:
- В простых проектах с CRUD-логикой
- Для быстрого прототипирования
- Когда вся логика — это «достать объект, изменить, сохранить»
- В командах, где нет времени на SQL-экспертизу
Но если вы работаете с аналитикой, отчётами, сложными связями — без SQL никуда.
Финальная мысль
Wozniak в статье задаёт правильный вопрос: если для эффективного использования ORM всё равно нужно знать SQL, то зачем ORM?
Я не призываю выкинуть все ORM завтра. Я призываю понять, что происходит под капотом. Не знать SQL в 2024 году, работая с базами данных — это как не знать, как работает двигатель, но водить машину. Можно. Но когда сломается — будете беспомощны.
ORM можно использовать. SQL нужно знать.
Ссылки
- What ORMs have taught me: just learn SQL — Adam Wozniak
- The Vietnam of Computer Science — Ted Neward (Object/Relational Impedance Mismatch)
Дмитрий Полухин — продуктовый дизайнер. Пишу про разработку, AI и дизайн интерфейсов. Обо мне, контакты и профили.