Проблема различия обьектной и реляционной модели настолько известный медицинский факт, что у этой проблемы даже есть своя статья в википедии:
- Транзакционность(изоляция обьектов в транзакциях), отсутствие механизмов контроля парралельного выполнения.
- Несоответствие императивного характера работы с обьектами в conventional ЯП(читай, предсказуемое время выполнения) и декларативного характера работы в реляционных субд(читай, непредсказуемое время и способ(при наличии неявных блокировок это критически важно) выполнения запроса).
- Наличие отдельного, независимого livecycle обьекта после его загрузки из РСУБД.
- Переизобретание хужших языков запросов и языков манипуляции(SQL vs JPQL/HQL).
Диспозиция:
На текущем проекте для слоя данных используем Hibernate а так же поддерживаем legacy plain-sql интерфейс на spring jdbctemplate. Намедни mysql база на диске перевалила за 30Гб. 300 таблиц в БД, ориентировочно 150 замапленых сущностей. Приложение - типичное веб-приложение на jsf. Сессия открывается на каждый запрос в spring используя OpenSessionInView(Interceptor). В проекте (пока что) строго поддерживается разделение между бизнес-обьектами(beans) и обьектами-сущностями из БД(entities). До выхода из ServiceFacade(который непосредственно или опосредовано - через DAO) можно работать с entity, любой код вне ServiceFacade должен работать только с бинами.
На лицо дублирование классов и наличие перекладывания данных из обьекта в обьект при получении из БД и при сохранении. (Case study 1) Чешутся руки дублирование убрать - в конце концов, hibernate entities декларируются как POJO.
Сделали. Словили множество
org.hibernate.PersistentObjectException: detached entity passed to persist
в местах где издревле проблем не было. Почесали репу, поняли что с политикой сохранения сущностей у нас бордель и беспорядок(если бы мы однотипно сохраняли-мержили все entities, пришедшие из другого persistence context, проблемы бы не было, но как такое отследить, когда граф передачи данных - спагетти?).
[...] we now have a second source of data identity, one in the database (protected by the aforementioned transactional scheme), and one in the in-memory object representation of that data
Я вижу rationale такого поведения. Мы храним все сущности, полученые когда-либо из БД в persistence context - нам надо отслеживать какие поля в обьекте изменились(с учетом каскадных зависимостей) и выдавать в СУБД оптимальный поток запросов на изменение сущностей при сохранении. Если бы не было каскадных зависимостей(т.е, вызвав .persist() у сущности, мог бы выполнится либо один либо ноль update сущностей в БД), вполне можно было бы обойтись insert on duplicate key update или другим пропиетарным решением.
Можно ли игнорировать эту проблему и вручную делать .merge() при возникновении detached entity exception? Полагаю, что это очень плохое, непремлимое решение. В случае, если обьект с таким ключом уже есть, он(если не повезет, то может быть уже совсем другой обьект из другой сессии) будет переписан(если не упадет constraint violation - мы же не актуализируем зависимые сущности). Кроме того ручной .merge убивает изоляцию данных между транзакциями.
Community предлагает решение - удлинять (persistence) сессии (вынести в http session или conversation context). Не вариант по причине Case study 2. Хотя persistence context является lightweight обьектом, данные которые в нем хранятся могут быть феноменальными по размерам - это же полная мапка обьектов в БД, да еще и адресуемые по strong reference. Это не говоря о том, что и database connections, и dbcp pooled connections со стороны java являются ценным и ограниченым ресурсом, которые необходимо беречь. Определить момент, когда пользователь больше не нуждается в persistence сессии невозможно - это stateless http - только таймауты на ресурсы. Pooled connections, занимаемые на 30 минут каждым клиентом, это похоже на рецепт катастрофы.
(Мифический Case Study 1.1) У нас существует два рассогласованых состояния обьектов - обьекты в РСУБД и обьекты в persistence context. Каким образом обеспечивается согласованая работа нескольких приложений, работающих с одной БД? Ответ - используя механизм транзакций. Да, у нас данные в persistence context актуальны только на момент начала транзакции. But thats ok - транзакционность такое предусматривает. Но что если наш уровень изоляции транзакций не предусматривает изоляцию данных, например у нас установлен уровень READ UNCOMMITED? Ой. У нас будут устаревшие данные в сессии и потенциально рассогласованые инсерты?
(Case study 2) В ходе профайлинга тормозов большого batch джоба, было замечено, что 40-70% времени приложение проводит в проверке наличия обьекта в сессии. Собственно говоря, это все. Сессия хранит все обьекты, которые когда-либо в ней загружались, проверка неименности обьектов нужна для .persist(). Приходится выкручиваться и писать уродский код.
if (i%10000==0) { session.flush(); session.clear();}
Почему ORM не может подумать о такой фигне сам? При наличии транзакций, flush и clear являются безопасными(хотя и не прозрачными) операциям. /*Важное замечание - да, мы работаем в транзакции и hibernate знает текущее состояние транзакции. Он знает, что делать flush до commit безопасно, но никак это знание не использует.*/
(Case study 3) Транзакционность не является механизмом обеспечения согласованности данных в БД - это механизм увеличения concurrency работы с СУБД, который работает хм... большую часть времени. Наверное вы видели
Deadlock found when trying to get lock; Try restarting transaction.
при работе с mysql. Текст ошибки очевиден - дедлок в момент (возмножно неявной) блокировки. В интернетах не видят проблемы с этой ситацией и считают ее нормальной - дескать C'est la vie - перезапустите транзакцию. Это практически всегда невозможно. Например, если в ходе операции вы изменили состояние внешней системы и внешняя система не может откатить состояние. Даже если мы просто изменили глобальное состояние приложения, такое зачастую невозможно. Это не Haskell и не Clojure - в java нет STM, операции не чистые и неповторяемые. Проблемы можно было бы избежать, используя общие принципы борьбы с дедлоками, например запрашивая локи в едином по всей системе порядке. Да, нам пришлось бы "do a lota monkey business" - искать неявные локи и преобразовывать их в упорядоченые явные. Увы, hibernate не только не занимается этим, но и не дает возможности явно вызвать пессимистичную блокировку на таблицы в нужном порядке(http://docs.jboss.org/hibernate/core/3.3/reference/en/html/transactions.html#transactions-locking). Предоставляя унифицированый API, он нас этим API сковывает.
(Case study 4) - моя любимая. Есть ситуации, которые в hibernate просто нельзя обьяснить с позиций логики. Стратегия получения элементов дочерних коллекций может быть определена с помощью Criteria.setFetchMode(join|subselect|select|batch). Какие изменения вы ожидаете, когда меняете FetchMode с, скажем, select на join? Думаю, что вы не ожидаете того, что hibernate начнет вам возвращать другое колчичество обьектов из БД. В официальной докуменации написано:
Join fetching: Hibernate retrieves the associated instance or collection in the same SELECT, using an OUTER JOIN.
OUTER JOIN! если в момент поиска одной сущности соответствует 300 других сущностей, которые находятся по join, но могут даже не участвовать в выбираемых колонках, вы получите в 300 раз больший результат, где пристуствтует по 300 копий одного обьекта. Если джойнятся две таких таблицы количество данных вырастает в 90000 раз. продолжайте ряд. -Если критично, - говорят разработчики, - сохраняйте выборку в Set, который сам все отфильтрует. То, что трафик между sql сервером и клиентом растет в тысячи раз и содержит одни и те же данные, их не волнует. -Можете использовать result transformer, который включит SELECT DISTINCT вместо SELECT на сервере. Про то, что DISTINCT практически гарантировано приведет к filesort разработчики, похоже, не знают.
Они специально это сделали так, чтобы максимальное количество разработчиков об это долбанулось? Просто добавив алиас на новую таблицу для критерия при поиске, вы меняете фактические результаты работы hibernate. Изменение нефункционального(чисто служебного) свойства ломает поведение. Почему, спрашивается, нельзя было сделать определение нужного типа join по маппингу? Любая база данных поддерживает left join, он такой же эффективный, он нормально работает. Даже если нет ресурсов для реализации нахождения графа зависимостей, почему тогда просто не сделать setFetchMode(leftjoin|innerjoin)? нет ответа.
(Tiny Case study 5) В Criteria нельзя использовать таблицы, которые не замаплены для этой сущности. Напоминаю, что в (detached) criteria мы ручками указываем имена таблиц и как именно нужно мапить. Разработчики молчат, и говорят, что это не готово. http://b23.ru/3btp Напоминаю, что этим разработчикам за работу над hibernate платят деньги.
(Tiny sidenote 6) В hibernate сейчас открыто 2689 багов из них 174 критикалов и блокеров. Сложность проекта и его codebase неадекватен решаемой задаче. Это просто какое-то хождение по минному полю в ожидании того, как вы разминируете какой-то из багов. Например, я так и не смог побороть https://hibernate.onjira.com/browse/HHH-1718 Stay tuned. Мы продолжаем хождение по мукам. Постараюсь пока не писать больше негативных постов. Попробовать mybatis, db4o и ebean, что-ли.