В прошлом году вернулся в родную экосистему Java, но годы, проведённые на джаваскриптовой чужбине, оставлили свой след, и теперь я смотрю на знакомые с профессионального детства края совсем другими глазами. Кое-что из того, что когда-то было привычным и естественным, теперь кажется странным и раздражающим. Например, ORM. В экосистеме Node.js эта концепция, насколько я могу судить, только начинает пускать свои ростки, а вот в джавовской экосистеме мы уже наблюдаем матёрый баобаб Hibernate, ставший, по факту, индустриальным стандартом. К моему огромному сожалению.
Казалось бы, а что не так? Java же построена на абстракциях высокого уровня. От программиста полностью скрыто управление памятью, например. Но проблема как раз в том, что ORM не способен полностью скрыть от программиста нюансы работы с базой! Если в случае с памятью программист может полностью полагаться на то, что виртуальная машина уж как-нибудь управится с выделением и очисткой памяти, если программист не очень будет ей в этом мешать, то в случае с ORM программист должен ооочень хорошо понимать, как ORM делает запросы, иначе потери производительности будут заметные (причём, заметные в продакшене, но не в тестовой среде). При этом предлагаемая ORM структура кода (к этому мы ещё вернёмся) побуждает программиста использовать именно неэффективные способы добычи данных. Инстинкт джависта толкает под руку и требует повторно использовать уже написанные функции, поэтому очень сложно не впасть в соблазн и не надёргивать данные кусками из нескольких DAO. Не говоря уже о том, что более сложные случаи, вроде агрегированных данных, ORM вообще не поддерживает! Крутись, как хошь: либо вытаскивай из базы многократно больший объём данных, чем требуется, и общитывай в памяти то, что SQL посчитал бы заведомо эффективнее (но зато всё красиво и typesafe), либо парси, как дурак, список из Object[].
К счастью, есть же ещё многоуровневый кэш, который, по идее, должен компенсировать эти проблемы: может быть, по началу приложение с ORM и будет ходить в базу чаще, чем надо бы, но потом наиболее часто используемые данные окажутся в кэше и производительность, глядишь, будет повыше, чем в случае оптимизированных запросов, результаты которых не кэшируются. Но есть некоторые нюансы.
Во-первых, а какова вероятность того, что требуемые данные будут взяты из кэша, а не из базы? Оставим в стороне случай, когда одни и те же данные выбираются снова и снова, потому что программист не сохранил/не передал дальше по течению данные, полученные в первый раз. Это уже вопрос культуры кода в обращении с ресурсами, обращение к которым затратно. Можно предположить, что эта вероятность пропорциональна соотношению ёмкости кэша объёму данных в базе, и пропорциональна вероятности того, что поступающие в систему запросы будут требовать выборки одних и тех же данных. Другими словами, в гипотетическом вэб приложении с объёмом данных в базе, намного большим, чем объём кэша на сервере, и с большим количеством одновременно работающих над разными данными пользователей эффективность кэша, как мне представляется, будет не слишком высока. Как оценить эффективность кэширования более или менее точно, я плохо представляю, потому что она зависит от факторов, не воспроизводимых в тестовой среде. Причём, оценивать имеет смысл только эффективность кэша второго уровня, потому что кэш первого уровня предназначен компенсировать самые вопиющие случаи неэффективного доступа к базе.
Во-вторых, кэш порождает свои собственные проблемы с масштабированием системы. Когда для повышения пропускной способности систему запускают на нескольких серверах, требуется как-то синхронизировать кэши этих серверов. Решения у этой проблемы есть, но, кажется, не вполне тривиальные.
И, в-третьих, данные из кэша достаются по айдишнику. То есть, все запросы, которые достают данные по другим параметрам, идут мимо кэша по определению. Quеry cache, конечно, существует, но я плохо себе представляю, в каких условиях его можно было бы использовать эффективно.
Выглядит так, что ORM порождает серьёзные проблемы с производительностью, решение этих проблем порождает проблемы с масштабированием системы, и что-то мне подсказывает, что это не последний виток рекурсии.
Ну ладно, может, это просто я чего-то не знаю или не умею.
Хуже другая проблема, которая нечувствительно заложена в самой идее ORM (или, по крайней мере, в самой распространённой трактовке этой идее). ORM, по сути, предлагает архитектуру, в которой данным из одной таблицы в базе соответствует только одно представление во всей системе. Пресловутый entity. И это специфическим образом организует всю систему. Вплоть до того, что структура пакетов получается такова, что весь код делится по принципу "принадлежности" к определённой entity-сущности. Этакие сваи, прошивающие веб-приложение от базы данных до UI. Да-да, до UI, потому что одному энтитю соответствует один DTO (с автоматически генерированной трансформацией entity в DTO и обратно) и один общий endpoint, задуманный согласно методологии REST и обслуживающий стандартные CRUD операции.
Собственно, в чём проблема? В том, что оное общее представление данных - это практически калька с формата хранения этих данных в базе. В общем случае оно не оптимально ни для каких других задач, кроме пресловутой трансформации из реляционной структуры в объектную. Про влияние структур данных на алгоритмы подробно объяснять, надеюсь, необходимости нет. Плохой (не подходящий под конкретную задачу) формат данных порождает плохой, переусложнённый и неудобочитаемый код. Попытки решить через такую систему задачи, чуть более сложные, чем простой CRUD, порождают кадавров, кошмарность которых прямо пропорциональна решимости архитекторов и кодеров "сделать красиво". Вместо того, чтобы выделить каждой конкретной задаче своё место в структуре кода со своими собственными структурами данных и своими собственными оптимизированными запросами в базу, городят нечто обобщённое, которое на каждом шагу пытается понять, кого оно обслуживает и чего от него хотят. Я видел целые самописные фреймворки, навёрнутые поверх Hibernate, чтобы было сподручнее чуть ли не с UI посылать Hibernate намёки (так и называлось - hints), какие дополнительные связи "главной" сущности надо тянуть из базы.
И это не следствие ошибок конкретных программистов, это следствие фундаментальных принципов ORM в его обычном виде. Чтобы эти проблемы решить, надо отказаться от понятия "сущности" в принципе. Формат данных должен гибко определяться сообразно каждой конкретной задаче. Трансформация в объектную структуру должна задаваться не для таблицы, а для каждой конкретой выборки. Кстати, такая возможность в Hibernate тоже есть, вот только ей никто не пользуется, и даже офичиальная документация этой возможности оставляет впечатление, что писали её "на отъявись".
Спрашивается, а почему тогда ORM так популярны, что их аж в Node.js экосистему тянуть пытаются? А потому, что для примитивного CRUD'а, прототипов и прочих HelloWorld'ов - самое оно. Код пишется быстро, выглядит красиво. А до задач, которые в эти рамки не влезут, ещё дожить надо. Сделка с дьяволом, которая довольно долго выглядит очень выгодной. А там и формат мышления соответствующий сформируется, и навыки появятся, как поудобнее устроится в аду.
Ну ничего, скоро все перейдут на NoSQL, и это порождение лени и начётничества канет в прошлое вместе с реляционной моделью данных.