ORM для реальных приложений не окупается
Идея упростить или абстрагировать код с помощью ORM, возможно, имеет очень ограниченный контекст применимости. По сути ORM хорош для приложений уровня простого CRUD, а дальше начинает только мешать. А CRUD-приложений в реальной жизни очень мало.
Проблемы
- При использовании ORM мы обычно прописываем в коде сущности и их взаимосвязи, и по сути это — проектирование БД ещё раз (дублирование логики!) прямо в коде.
- Борьба с проблемами производительности никуда не денется всё равно, как ни абстрагируй. Ты просто не можешь не знать, что у тебя под капотом происходит. Какие там делаются джойны и группировки.
- Язык запросов в виде цепочки объектов и методов читается хуже, чем 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