Как стать автором
Обновить
0

ORM для реальных приложений не окупается

Время на прочтение4 мин
Количество просмотров31K


Идея упростить или абстрагировать код с помощью ORM, возможно, имеет очень ограниченный контекст применимости. По сути ORM хорош для приложений уровня простого CRUD, а дальше начинает только мешать. А CRUD-приложений в реальной жизни очень мало.


Проблемы


  1. При использовании ORM мы обычно прописываем в коде сущности и их взаимосвязи, и по сути это — проектирование БД ещё раз (дублирование логики!) прямо в коде.
  2. Борьба с проблемами производительности никуда не денется всё равно, как ни абстрагируй. Ты просто не можешь не знать, что у тебя под капотом происходит. Какие там делаются джойны и группировки.
  3. Язык запросов в виде цепочки объектов и методов читается хуже, чем SQL, по сути это — особый язык, который надо учить. За себя скажу, что когда писал на PHP (Laravel), длинные запросы на Eloquent меня иногда изумляли своей сложностью чтения:

$query = Ad::
select(array('ads.*', DB::raw('COUNT(DISTINCT clicks.id) as clicks_count'), DB::raw('COUNT(DISTINCT shows.id) as shows_count'), DB::raw('(COUNT(DISTINCT clicks.id) * COUNT(DISTINCT shows.id))/100 as CTR')))
->leftJoin('clicks', function($join) use($date){
  $join->on('ads.id', '=', 'clicks.ad_id')->where(DB::raw('DATE(clicks.created_at)'), '=', $date);
})
->leftJoin('shows', function($join) use($date){
  $join->on('ads.id', '=', 'shows.ad_id')->where(DB::raw('DATE(shows.created_at)'), '=', $date);
})
->groupBy('ads.id')->with('devices', 'platforms');

В итоге, кстати, некоторые производители ORM даже пытаются прикрутить свой собственный абстрактный недоSQL на объектах бизнес логики, например, как в DOCTRINE:



$query = $em->createQuery('
    SELECT u 
     FROM Doctrine\Tests\Models\Company\CompanyPerson u 
     WHERE u INSTANCE OF Doctrine\Tests\Models\Company\CompanyEmployee');

или Hibernate:


IQuery q = s.CreateQuery("from foo in class Foo where foo.Name=:Name and foo.Size=:Size");

В итоге непонятно, для чего козе боян, ведь обычный SQL — это и так абстракция над бизнес-сущностями и их взаимосвязями, и обвешивать это ещё одним слоем с птичьим языком (или тем более с птичьим SQL-языком), игнорируя все проблемы перформанса — это просто странно. А под капотом ORM делает иногда такое, что волосы дыбом.


Странная абстракция


Есть ещё такое мнение, что ORM — это слой, абстрагирующий от способа хранения. Мол, сегодня ты пишешь на MySQL, завтра на Postgres, после завтра вообще в файлах хранишь — и тебе пофиг, код остаётся тем же. Чистая архитектура.


Ну это вообще обычно ерунда, конечно. Уродовать код и замедлять разработку, чтобы с вероятностью 0.01% захотеть переехать на другую базу — ну такое.


Про чистоту архитектуры тоже можно вставить 5 копеек, чтобы два раза не вставать. Очень часто слои пересекаются. Как ни крути, но делать абсолютно 100% независимый слой бизнес-логики (юзкейсы, сервисы) иногда бывает очень дорого. Например, если тебе надо построить хитрый отчёт, ты будешь использовать SQL с группировками, оконными функциями, фильтрами и джойнами, выжимая из базы данных всё, что можно, включая грязные хаки. Там будет не до абстракций. Да просто сделать group by и посчитать количество тех, у кого count больше одного — это ведь уже бизнес-логика, вшитая в SQL.


НО


С другой стороны, писать совсем простейшие запросы вручную задалбывает, конечно, поэтому какой-то гибридный подход, наверно, может и подойти в некоторых ситуациях. Но даже в простых вещах нужно быть осторожным: на практике встречал удивление а-ля "а почему у нас при формировании этой страницы к базе уходит 254 запроса, мы ж только табличку вывели?"


В мире Go


На данный момент в Go хоть и существуют ORM (например, gorm.io/gorm), но большинство команд ими не пользуется. Просто потому, что в языке Go на любую магию традиционно смотрят с подозрением. По задумке авторов Go простой как дрова.


При этом проблемы сырых запросов решаются точечно. Где-то просто пишется SQL, но, например, если нужно запрос составлять из частей по определённым условиям (например, фильтры, прилетающие из формы), можно использовать squirrel (query builder):


users := sq.Select("*").From("users").Join("emails USING (email_id)")
if onlyActive{
    users = users.Where(sq.Eq{"deleted_at": nil})
}

sql, args, err := active.ToSql()

Это безопаснее и проще, чем конкатенировать из частей. Тем не менее это именно queryBuilder, а не ORM, т.е. нет отображения сущностей на реляции, нет магических неявных джойнов под капотом.


Если же надо получать из запроса структуру данных, не перечисляя каждый раз поля, то можно это сделать с помощью sqlx:


  people := []Person{}
  db.Select(&people, "SELECT * FROM person ORDER BY first_name ASC")

Здесь есть немного магии, так как библиотека используется reflection, но это чисто технический, не особо принципиальный момент.


Выводы


Тема БД всегда очень сложна и холиварна. В проектах Каруны есть разные языки и разные команды, и подходы к работе с БД тоже сильно отличаются. На Руби ORM используется в основном очень плотно, на Go его почти нет, и т.д. Поэтому выражу только своё персональное мнение: в реальных (не CRUD) проектах абстрагироваться от данных в базе бывает слишком дорого (читабельность и производительность), а ценности такое абстрагирование даёт очень мало, ведь SQL сам является абстракцией над бизнес сущностями.


Самое забавное, что использовать или нет — это скорее вопрос традиций в конкретном стеке.


Кстати, часто слышал, что в C# какой-то особенный подход к ORM, и там всё супер. Буду рад, если поделитесь реальными впечатлениями в комментариях. К сожалению, мало что знаю про этот язык.


Статья является компиляцией идей из канала Cross Join

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 75: ↑48 и ↓27+30
Комментарии231

Публикации

Информация

Сайт
karuna.group
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия

Истории