Pull to refresh

Comments 46

Столкнулся с такой задачей. Сервисы, которые используются для веб контроллеров и rpc/веб сервисов. В контроллеры можно отдавать объекты с ленивой зарузкой, а вот как красиво сделать префетч нужной «глубины» для удаленных вызовов — вопрос интересный оказался. Кроме того «глубина» зависит еще и от «глубины» нахождения самого объекта (т.е. описывая правила для объекта А, мы можем возвращать объект A или объект B, который по зависимостям вытягивает объект A).
UFO just landed and posted this here
Можно при выгрузке объекта через HQL попросить Hibernate выгрузить его сразу вместе с детьми.
Рекурсивных детей не соберешь. Для простых решается, как упоминолось раньше, тупым get().
Вопрос глубины довольно неприятный, если есть рекурсия, то бишь если объект ссылается на себе подобных, которые, соответственно, могут ссылаться опять же на себе подобных.
Тут, скорее, надо помечать, что надо грузить, а что нет.
Но как? Для простых объектов это можно нарисовать на стадии описания класса. Но это редко надо — тогда этой проблемы вообще нет. Реально нужно указывать, что грузить, а что не на стадии загрузки данных. Тут наверно нужен двойной подход: имеем в описании класса сразу метки, что грузить, а что нет — не более одного уровня вниз. И кроме того указываем (метим) то, что нерекурсивно и можно грузить, а что — нельзя. Далее в момент загрузки указываем, если надо, на какую глубину рекурсии работать (по умолчанию работает первый уровень и все тихо и спокойно) — и имеем те данные, что хотим передать.
Небольшой пример. Например в юниксе имеем user и group. User может состоять в одной или нескольких группах. Group соответственно содержит пользователей. А пользователь опять же состоит в группах. Ну и т.д.

Итак, для проверки например пароля нам незачем знать других пользователей — нам нужен фактически только логин и пароль конкретного пользователя. Тогда метим группы как рекурсивные и не требующие загрузки.

Второй момент. Нам надо знать всех пользлвателей со всеми данными из конкретной группы. Рекурсия. По идее для группы пользователи помечены как рекурсивные и их не надо подгружать из базы (для задачки «нам надо показать только список групп без инфы пользователей»). Но в данном случае мы хотим большего.
Тогда при загрузке явно указываем, что надо пройти на уровень +1, то есть самих пользователей. Если же хотим еще больше — в каких групах этот пользователь состоит (блин, это уже изврат конечно), то указываем +2.

Тут уже возможны варианты по имени свойства и т.п. — это на усмотрение.
Еще можно создать TO, который собственно будет содержать нужную информацию, которая нужна на клиенте. для этого создаются хелперные трансформеры вида

...
public static getTO(MyEntity entity) {
      MyTO tobj = new MyTO();
      
      tobj.setFirstData(entity.getFirstLazyData());
      ....
    }
...


и вызываются у service уровня. Сессия там понятно есть.

Понятно, что создаются лишние классы transfer objects, но такова плата. Зато у нас меньше всякого reflection'а, все под контролем, нет неожиданных LazyInitExceptions
Простите за ничего не возвращающий метод, но вы поняли идею. :)
Решением в данной ситуации может послужить создание небольшого класса-утилиты, который будет выполнять депроксирование домена путём создания нового объекта того же класса и инициализации свойств, соответствующих базовым классам Hibernate’a. Все остальные свойства объекта будут инициализироваться слоем служб по мере необходимости.

Примерно это я и предлагаю. С той разницей, что объекты DTO будут экземплярами того же класса, что и домены предметной области. Относительно вашего примера MyTO = MyEntity.
Я столкнулся с данной проблемой во время разработки WEB-приложения.
Разрабатывал на Spring + Hibernate.
Решил проблему таким образом:

Система делится на слои:

Controller
Service
DAO

Каждый метод в Service является логически завершённым. Они проектировались так, чтобы за его пределами к этой группе данных обращаться было уже не нужно.

Методы Service обращаются к 1 или более методам DAO (За исключением тех которым не нужно обращаться в БД).

С помощью аннотации @Transactional, которая предоставляется Spring`ом, каждый метод Service работает в одной транзакции и, соответственно, внутри него сессия открыта. Можно свободно получать доступ к lazy-полям.

Controller получает уже готовые наборы данных для передачи их в представление.

Таким образом получается работа без костылей и, одновременно, хорошо структурированный по логическим блокам код в Service-слое.
Ну с веб-приложением такой проблемы и нет. Используем Session-per-Request, и все разруливается само по себе. А вот совсем другое дело (как в случае у топикстартера) — когда уже на клиенте (в клиент-серверной архитектуре) понадобились свойства, которые могли быть не инициализированы.
Spring работает в десктопных аппликациях тоже ))
Я понимаю, но BVadim говорил о «разработке WEB-приложения».
А разве для веба это как раз не проще?

Ведь веб-клиент ну очень сильно подталкивает выполнять очень полезный принцип — «No Distributed Objects».

То есть просто данные клиенту нужно передавать.

Тогда ключ — в продуманном API:

— хочет клиент просто пользователя — передаем ему пользователя, группы — как id;
— хочет всех пользователей со всеми группами — передаем всех пользователей и все группы.

Да, клиент при этом сам увязывает объекты по переданным id, при необходимости сам реализует lazy load (если, например, AJAX работает). Но это и правильно: только клиент знает, что в какой момент ему понадобится.
А я разве говорил что-то против такого подхода для веба? Я согласен с BVadim, он использует Session-Per-Request, я сказал просто, что для клиент-сервера такой подход не подойдет.

Я полностью за отсутствие Distributed Objects (в том смысле, чтобы объекты не выходили за определенные рамки).

Насчет того, что «только клиент знает, что в какой момент ему понадобится.» — то это не совсем верно, ибо знает разработчик, а не клиент. Конечно, было бы здорово, если бы fetching-стратегии вообще формировались сами наиболее оптимальным образом, но на практике все всегда оказывается сложнее, чем нужно. И, к примеру, в случае NHibernate мы используем паттерн QueryObject не только для реюза запросов для отдельных ситуаций, но и для того, чтобы инкапсулировать в них все NHibernate-специфичные штуки (включая fetch-стратегии). И не паримся по этому поводу, потому что серьезное изменение логики UI часто влечет корректировку fetching-стратегий, и давно понимаем, что не стоит делать что-то мега-универсальное, нужно действовать по ситуации.
Приношу извинения — недопонял, что Вы как раз для веба это и допускаете)

Под «клиентом» имею в виду в любом случае вызывающий удаленный код — даже Веб — это клиент-сервер. Ну и _теоретически_ принципы сохраняются и для более толстого клиента, но на практике много разных вынужденных отклонений, конечно.

А так с Вашим решением вполне согласен, только вопрос: Вы создаете специфические QueryObject для специфических методов API, или же, например, fetch-стратегии через конфиг привязываете к методам API и нужные передаете в более общие QueryObject?
Последний вопрос про QueryObject можете уточнить немного? Не совсем понял, о чем именно вы спрашиваете :)
А зачем куда-то там «передавать» сессию? Если речь опять же о вебе, то в идеале данные готовятся строго до View, а контроллер возвращает ViewModel. Все уже подгружено, загружено и трансформировано для View. Если же что-то все равно вызывается внутри View, то если у нас Session-Per-Request — то в момент формирования View сессия должна быть еще открыта.
Это именно как Session-Per-Request делается в Спринге. Согласен, что это не очень хорошее архитектурное решение и сам не использую, но вполне представляю, что в некоторых случаях может пригодиться.
А почему нельзя рассматривать по ходу появления прецедентов? Ну, то есть, для конкретной ситуации делать соответствующий fetch всех свойств необходимого уровня вложенности?

Для чего нужно изобретать абстрактный механизм lazy-инициализации над уже существующим (реализованным с помощью Dynamic Proxy — в NHibernate, в Hibernate, как я понимаю, один в один то же самое)? NHibernate/Hibernate сам по себе позволяет довольно гибко определять Fetch-стратегии.
Гм, прокси-объекты не из воздуха создаются же, в чем проблема записать в него информацию об источнике данных, откуда взята сущность, и при обращении к данных, скрытым за прокси-объектом, брать их из этого источника? проблема в наличии транзакций и сессий? Или я чего-то недопонял?
Проблема в том, что к сессии хибернейта имеет доступ только сервер, а данные надо подгуржать на стороне клиента. А если у тебя нет сессии хибернейта, то поле ты уже не инициализируешь.
Ну можно еще реализовать паттерн Session-Per-Conversation, но это очень «тонкое восточное искусство», будем надеяться, у автора топика не такой запущенный случай :)
«в чем проблема записать в него информацию об источнике данных, откуда взята сущность»
Жесть какой хак, если честно :)

Прокси-объект и DTO-объект — разные штуки. В Proxy ничего записывать не надо. Hibernate должен сам следить за его целостностью и временем жизни.

Как CrazyViper уже отметил, суть в том, что когда данные понадобились, сессия на сервере уже закрыта (что, впрочем, архитектурный вопрос и сильно зависит от конкретной системы). Проблема с LazyInitialization возникает в случае использования detached-сущностей, когда они уже «оторваны» от сессии.
Плохое решение. Нормальное решение — не пускать персистентные объекты дальше границ сессии. Я обычно использую фассад, отдающий DTO. Это решение имеет еще и дополнительный плюс — меньшая связанность.
именно. я об этом свой коммент тоже написал
DTO-это стандартное и правильное решение. Но часто люди хотят упростить себе жизнь, имея доступ прямо к объектам. Как-то стояла задача отдавать объекты хибера апликации GWT. Для этой цели я использовать Gilead, который автоматически депроксирует хиберовские бины, заменяя не только проксированные ссылки, но и коллекции.
Именно так. В общем случае в качестве DTO выступают не энтитеты, а POJO Бины, которые в совокупности отображают ту часть графа объектов, которая интересна клиенту.

Да, и еще, советую попробовать Dozer для автоматизации мэппинга Entity->DTO и наоборот.
Удобная штучка, хорошо себя зарекомендовала в боевых условиях (у нас тут почти 6000 таблиц...) И, походу, «наш ответ» автору статьи.
На мой взгляд, в качестве классов для DTO вполне можно использовать те же классы, что и для доменов (создавая их вручную, так что бы они не были обёрнуты в прокси). Это уменьшит количество кода и сделает его более унифицированным. Да, это увеличит связанность между клиентом и сервером, но… я вполне допускаю ситуацию, в которой клиент знает о доменах сервера.
а почему на клиентской стороне не реализовать proxy объекты с обращениями к серверу на гетерах/сетерах?
Потому, что простые решения работают предсказуемее.
А как именно вы это представляете? Не проще ли в поддержке будет держать границы сессии там, где нужно, и использовать только DTO/ViewModel'и для передачи данных?
Гофофский HOPP я имел ввиду. Идея передавать клиенту все данные, а он сам разберется, какие нужны — звучит не здорово.
Что значит «клиент разберется»? Клиент запрашивает, а сервер отдает ему только то, что нужно в конкретной ситуации.
Вы передаете объект с сервера на клиент и сталкиваетесь с проблемой депроксирования? Может я чего-то не понял, но если использовать веб-сервисы таких проблем вообще нет. По мне, пример передачи объекта на клиент за уши притянут как оправдание депроксирования.
>Сами классы доменов доступны как серверу, так и клиенту (что, в общем, логично)

У них один класслоадер? Если нет, то это разные классы.

>… компоновки данных для клиента: создание DTO-объектов на базе классов доменов с помощью копирования определённой глубины и детализации

Не уверен, что на клиенте вообще оправдано иметь ту же структуру классов, проще было бы на клиенте делать классы без ссылок, простейшие DTO, которые нужны только для отображения данных. Вот вы сделали на клиенте такие классы как на сервере, т.е. с сылками, значит на клиенте рано или поздно возникнет NPE, так как на клиенте нет хибернатовых прокси.
1. Клиенту и серверу доступен один и тот же jar'ник с доменами. Но это не важно.
2. Хм… вы уже третий, кто отписался в каментах за самостоятельные DTO-классы. Спасибо, я обязательно подумаю над этим.
В одном из проектов на строне клиента с помощью cglib переопределил методы у проксирующих хибернейтовских классов (на подобии PersistCollection, PersistSet, ...) методы доступа к лейзи объектам, при выбросе исключения LazyIniExc производится автодозапрос с сервера недостающих данных и ложится в нужное место проксирующего класса. Т.о. работа на клиенте становится идентичной работе на сервере с открытой сессией, дозагрузка недостающих данных происходит только при обращению к ним на клиенте.

Если интересен код реализации, могу скинуть чз 2 дня как вернусь с командировки, пишу с телефона.
Да, подход возможный, но на мой взгляд, слишком сложный.
Нет, ну технически, конечно, это выполняет задачу, возможно даже в какой-то мере масштабируется.

Но использование исключений для управления потоком выполнения, то есть ожидание исключения для обычного штатного случая — это проблема с архитектурой.
обычный перехват события того, что данные не загружены. Можно и не ловить исключение, а просто проверить есть ли они ли нет другим образом — что само собой происходит перед выбросом LazyIniExc.
никакой проблемы не вижу, в чем конкретно проблема?
Проблема именно в том, что «исключения» используются для «неисключительных» ситуаций. Если нужно событие — нужно использовать именно Event-обработчики/observer'ы и все такое прочее, но не Exception'ы.

Исключение — на то и исключение, чтобы обрабатывать именно исключительные ситуации.
Грубо говоря — ваше решение применить можно, но как fallback — то есть, в идеале в работающей системе этого произойти никогда и не должно.
Я просто подлезал и заворачивал ленивые коллекции. На попытку дёрнуть проверял на инициализацию, открывал сессию (если закрыта) инициализировал и закрывал сессию (если была закрыта). Во время работы столкнулся с нюансами которых было много… :)
Где вы были с такой статьей 3 года назад? я все эти преграды пробивал собственной головой… хотя вышло не плохо :) Столкнулся с этой проблемой при работе с Glassfish и JPA с hibernate. Очень хотелось использовать объекты домена в сервлетах, но выскакивало вышеназванное исключение. Пришлось ограничить самый верхний уровень бизнес логики только простыми типами данных, без proxy объектов.
Sign up to leave a comment.

Articles