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

Комментарии 35

entity = (T) ((HibernateProxy) entity).getHibernateLazyInitializer().getImplementation();

если я правильно все понимаю, то получается что на каждый элемент списка генерируется запрос в базу, что не есть хорошо
Если я не ошибаюсь, то при автоматической инициализации объектов Hibernate'ом для каждого объекта из списка составляется свой запрос.
В конкретно этом случае — мы можем запросить и целый список объектов отдельным запросом. Но не факт, что мы опять не получим прокси-объекты.
Ну вобщем то Hibernate создает прокси список который не содержит объектов, а потом уже идет select по FK и создается список при вызове геттера.
Мне кажется такое вот решение будет дико тормозить на большом количестве объектов.
На счет прокси-списка все верно. При запросе списка геттером Hibernate заполняет его объектами. Но некоторые из этих объектов могут быть не инициализированными. И при вызове instanceof для одного из них, мы можем столкнуться с proxy-объектом.
Только что проверил создаваемые hibernate'ом запросы при вызове геттера. Выбор идет по PK каждого объекта из ленивого списка. На каждый объект — отдельный запрос.
Для проверки выставлял следующие уровни логгирования:
  • org.hibernate.SQL=DEBUG
  • org.hibernate.type.descriptor.sql.BasicBinder=TRACE
а откуда же он взял эти PK, если при FetchType.LAZY не генерятся join'ы? у вас какая то магия выходит
Точно странно. У меня в логах hibernate идет запрос «User», вызываю геттер — появляются запросы телефонов по их PK. А где он эти PK взял не ясно. Отдельного запроса списка PK не было.
Займусь вечером изучением этого вопроса.
Разобрался в этой теме более подробно. Проверял на рабочем коде, отключил весь описанный в статье код. Алгоритм работы следующий:
  1. вызываем геттер у «User» списка «Phone»
  2. Hibernate выполняет запрос, который включает в себя все поля столбцы базового класса, join'ит всех потомков и выбирает у них по 2 разных столбца, дополнительно в запросе идет проверка к какому классу принадлежит строка
  3. для каждого объекта из списка вызывает запрос с его уже известным PK, к его таблице. В запросе собираются недостающие столбцы объекта
И да, использовать LinkedList для временного хранения нынче круто?
Здесь код представлен для примера. Там многое можно оптимизировать.
Основная нагрузка статьи — вариантов борьбы с описанной проблемой.
Проблема, конечно, интересная. Решение, конечно, некрасивое. Но меня терзают смутные сомнения, что тип телефона необходимо определять через instanceof. Вообще, логика, построенная на instanceof, попахивает.
Кроме того, насколько я помню, instanceof довольно медленная операция.
Вы серьезно? В данном случае, когда генерируются тонны запросов к дб (благодаря Hibernate.initialize в цикле...) bottleneck явно в другом месте
Ситуации бывают разные. Когда функционал должен быть готов вчера особо не задумываешься по поводу рефакторинга.
По крайней мере, можно заменить на проверку по getClass(), если не надо подклассы чекать.
В том то и дело, что нужны именно подклассы. То есть мы ожидаем CellPhone или SatellitePhone, а имеем прокси для Phone. И здесь никакой вид проверки не помогает.
А ещё в Hibernate есть такой режим ленивых ассоциаций, как no-proxy (включается аннотацией @LazyOtOne(LazyToOneOption.NO_PROXY). Правда, для их правильной работы требуется включить bytecode instrumentation, но в этом нет ничего страшного. Ещё один недостаток — требуется всюду не забывать указывать эту Hibernate-специфическую аннотацию, но даже этот недостаток можно преодолеть.
Спасибо за совет. Как будет время обязательно проверю этот способ.
Пробовали запустить на хотя бы пару тысячах элементов? Тормозить будет жууутко. Вообще, в местах где нужно делать подобного рода проверки, лучше делать join'ы сразу.
Hibernate.initialize в цикле это плохо. Замените хотя бы на инициализацию сразу целого списка (вернее Hibernate умеет делать fetch не более «batch size» элементов)
а при чем здесь batch? O_o
Наверное нужно было сказать 'fetch size'
Нет, именно hibernate.jdbc.batch_size :)
batch_size никак не используется для select'ов. Он используется для операций требующих executeUpdate
песня совершенно о другом
и о чем же?
Хорошо, не hibernate.jdbc.batch_size, а просто batch-size. Суть та же
Вы сами то ссылку читали?
Там написано, что если есть какие то объекты в сессии у которых есть Lazy поля, то при выборке этого поля для какого нибудь объекта, могут быть заодно выбрана такие же поля для других объектов в сессии.
Вобщем полное непонимание вопроса выходит у вас…
Вы попробуйте сначала это в практике, а потом говорите, что у меня «непонимание вопроса»
а чем это отличается от того, что я написал? и если ни чем, то как это относится к топику?
Посмотрите в разделе batch-size на пример «Another example» — это как раз то, что делает автор вызовом unproxy в цикле
В большинстве случаев, у нас объектов в таких списках меньше сотни.
В случаях когда объектов намного больше, у нас используются запросы по частям (например, по 100 объектов) и следующие части запрашиваются и результаты отправляются клиентам только по требованию.
Отвечая на ваш вопрос: нет, не проверяли.
Использую Eclipselink вместо Hibernate, он проксирует только на уровне списков. Но в целом проблемы те же: что делать с lazy объектами вне сессии, и как избавиться от N+1 запросов. Connected-архитектура и lazy инициализация — это огромный антипаттерн, который лимитирует возможность использования объектов только внутри сессии, постоянно напрягая БД огромным количеством тупых запросов. И до сих пор создатели JPA не предусмотрели хорошего способа для обхода ситуации — видимо те, кто пишут JSR, ориентируются на сферического коня в вакууме.

Мы делаем так: при вызове сервиса сначала вытаскивается все, что нужно и только то, что нужно одним или несколькими запросами. Для избавления N+1 запросов можно использовать join fetch или batch-fetch. В JPA 2.1 добавили EntityGraphs позволяющие более просто указывать relations, которые надо вытащить при запросе. Плюс есть нестандартные load-groups и fetch-groups. Если поле lazy и не проинициализировано, оно не должно использоваться. Затем service interceptor прогоняет граф через специальный фильтр, который пробегает все поля, обнуляя непроинициализированные прокси и заменяя проинициализированные коллекции на ArrayList и LinkedHashMap. На выходе получается полностью портабельный detached граф объектов. Почему EntityManager.detach() не делает то же самое — для меня загадка.
Всё-таки, на какую глубину графа надо доставать объекты, ORM-движку трудно судить. В вашей конкретной ситуации, это может быть и элементарно, но в общем, не так уж и очевидно. Видимо поэтому, разработчики ОРМ отдают это на откуп разработчикам приложений. А те уж используют DTO и все такое.
> Видимо поэтому, разработчики ОРМ отдают это на откуп разработчикам приложений.

В том-то и дело, что не отдают. В JPA нет хорошей возможности сказать что конкретно и как доставать. До JPA 2.1 не было даже стандартного способа указать, какие атрибуты мне нужны, а какие нет. Были vendor-specific query hints, и работали через пень-колоду. А основная N+1 проблема до сих пор не имеет решения: JOIN FETCH присоединяет только одну коллекцию. В EclipseLink есть batch hint, который внезапно не работает для ManyToOne и OneToOne.

Видимо, разработчики JPA надеялись, что у нас будет один большой кеш, где будет лежать 80% данных всей базы, с объектами которого будет работать приложение, потихоньку подгружая недостающие части. Однако, как показывает практика, любая страничка с простой таблицей начисто рушит данный подход.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории