Pull to refresh

Comments 43

Довольно полезный материал, хорошая подача, спасибо, плюсую.
Только исходник лучше по человечески куда залить (так и не добрался до него), хотя бы на pastebin.com, или github.
И за меня тоже плюсаните )
Было бы чем — плюсанул бы, хоть и дотнетчик. Динамика роста рейтинга этой статьи в сравнении с соседними интригами, скандалами и расследованиями про оракл и школонейросетями удручает.
И наводит на мысли о аудитории хабра.
Я сейчас тоже заметил что кармы не хватает, слили поганцы за последнее время :)
Не хочу показаться говнистым, но вы, часом, не мешаете все в кучу на ДАО уровне? Сериализация/десериализация в вашем случае, это вообще-то уровень презентационный. Т.е. по хорошему мухи отдельно, а объекты для сериализации вне Hibernate контекста.

Конечно, вы возражаете:
Можно, но придется писать свое DTO для каждого конкретного случая. И вообще, использование DTO должно быть получше обосновано, ведь это своего рода антипаттерн, т.к. вызывает дублирование данных.

Но это не является дублированием данных.

Потому что это представление этих данных в том виде в котором они уходят пользователю. (например у вас поле Имя и Фамилия в БД, а в сериализуемом объекте одно поле Имя Фамилия). Это раз.

Два — это организация слабосвязанности уровней, что делает архитектуру более упорядоченной и доступной для расширения.

Если у вас объекты точь-в-точь БД запись, ок — вы можете использовать Dozer для перекладывания данных за вас. Но тем неменее, разные уровни — разные объекты.

Ну или хотя бы выдавайте из ДАО уровня immutable DTO вне контекста Hibernate через ResultTransformer.
Уважаемый, я с вами полностью согласен. Все в кучу на DAO-уровне мешать не следует. Только вот статью эту я написал не для демострации того, какие уровни можно сделать, а как заставить JAXB не сериализовать ленивые коллекции. Кусочек кода из DAO приведен только для того, чтобы было понятно — объект Company мы можем получать как с ленивой коллекцией customers, так и с ленивой коллекцией suppliers. Код для service-слоя и контроллеров просто не приведен, — он слишком spring-специфичный и к теме статьи не относится. Если же вас все таки интересует, как у меня сделано разнесение по слоям — скачайте исходник. Если не сможете, — залью на github, а то тут уже жаловались. Обсудим все ваши замечания.
Эээ, я просто подошел к вопросу глобальнее. Раз у вас проблема LazyInit, то это не сериализатор виноват, а с тем что вы ему отдаете. Тем более, что вы сами указывали этот подход, как способ решения.
Согласно статьи подход с DTO указал гугл.
Конечно, было бы круто отдавать сериализатору всякий раз то, что он может съесть. Более того, если такой способ существует — так и надо делать. Мне просто хотелось привести пример, когда пункты 1-5 из гугла не прокатывают или смотрятся не очень хорошо.
Да, насчет hithub была отличная мысль — hls github
Конечно, было бы круто отдавать сериализатору всякий раз то, что он может съесть. Более того, если такой способ существует — так и надо делать

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

Код:
return Hibernate.isInitialized(value) ? value : null;

вообщем-то эквивалентен чему-то где-то далее:

try {
  return accessor.get(...);
} catch (LazyInitializationException ex) {
  return null;
}


Это же костыль?
Это не костыль, а метод борьбы с ними.
В случае JAXB и одного представления всякие DTO избыточны, все разруливают аннотации, другое дело если одни и те же классы нужно представить по разному (разный формат XML), тут уже нужно немого думать как лучше сделать.
Аннотации здесь особо не причем и DTO тут избыточны только эстетически. Проблема — это LazyInit.

Возникает она только потому, что производится попытка почитать не с того уровня. Ну это как если вы скачали имейл со ссылками на картинки, и выключили интернет, а потом открыли письмо и с картинками пролетели. Т.е. картиночки-то по хорошему надо отдавать вместе с письмом или инет не выключать.

Так и тут — для рендеринга куда бы то ни было (html, XML, JSON) все данные уже должны быть к этому моменту. А то что они lazy и еще и не подгружены — то это нонсенс в месте, где происходит сериализация, а решение — костыль.
Ну вот, мы и добрались до места, где участники дискуссии начинают говорить то, что они на самом деле думают о посте)
Еще раз повторюсь, если можно загрузить все данные, то того, как сериализатор их слопает — это хорошо, так и надо делать. Но судя по количеству вопросов в гугле типа «jaxb lazyinitialization что мне делать» — не всегда (или не у всех) это получается. Я вовсе не хочу, что бы предложенное мной решение тиражировалось на все возможные случаи. Но иногда оно может быть полезным и… более элегантным что ли, чем использование DTO, которое по вашему мнению я незаслуженно обидел.
Да, оставим предрассудки :)

Как я сказал выше, «если можно загрузить все данные… — это хорошо» значит что почему-то контроллируете логику приложения не вы, а хибернейт с управлением сессиями, и вы к этому приспосабливаетесь. Хотя должно быть наоборот

В запросах «jaxb lazyinitialization что мне делать» главное не jaxb + lazyinit, а просто lazyinit.
Максимализмом тянет. От проблем с LazyInit часто не избавиться, с какого бы боку дынные не тянулись, у меня сложилось впечатление что вы мало работали с Hibernate и не совсем его понимаете. Допустим представьте что я хочу достать только саму сущность (в которой может быть очень много Lazy связей на другие сущности) без каких либо связей на другие сущности/таблицы, и сразу нарисовать его в XML, как тут можно контролировать логику? По мне так смысл этой статьи в существовании аннотации @XmlAccessorFactory, а LazyInit лишь пример ее использования.
Хе-хе. Вы зачем вообще Lazy используете, в принципе? Вы должны понимать, что lazy поле — это не гарантия, что это значение не запросится при инициализации объекта из базы. Поэтому требования «я хочу получить только сущность без связей» — не может быть гарантировано.

Для реализации «получить саму сущность, без полей… и сразу нарисовать его в xml» пользуйтесь DetachedCriteria с ResultTransformer'ом в класс, который помечен Jaxb аннотациями. Он будет не в контексте Хибера и костылей по обходу того, что он не проинциализировал поле, вы избежите.
Ваше решение это defacto DTO. Придется определять 2 класса для этих DTO, которые вы будете подсовывать ResultTransformer'у. Да можно, вариант. Но почему вы отказываете AccessorFactory быть вариантом? Тоже работает, код не портит. Сфера применения, как уже верно отмечено, одним lazyinit не ограничивается.
Но почему вы отказываете AccessorFactory быть вариантом?

Потому что это сильный coupling: получается что какой-то AccessorFactory из вообще JAXB знает, что-то про вашу доменную модель, что там есть lazy и хибернейт. Вы там импорты, я вижу, покоцали, иначе там бы был импорт Hibernate пакета. И это есть сигнал.

Т.е. я даже вот просто открываю класс AccessorFactory из пакета не DAO, не Model, а JAXB и вижу там какую-то логику завязанную на Hibernate. Ыть!

Другое дело, что иметь возможность заменить правила сериализации — это ок. Но решать проблему lazyinitialization в JAXB сериализаторе — это нарушает loose coupling компонентов системы.
По моему, вы чересчур сгущаете краски. AccessorFactory в пакекта model тоже быть не должно. Это что-то вроде util. Что касается coupling: во-первых, без coupling вообще ничего не работает, во-вторых: я не вижу ничего страшного в том, чтобы в класс, предназначенный для работы с lazyinit, сделать import (один-единственный, кстати) hibernate'ового класса (или static метода).
В целом, решение с AссessorFactory дает очень небольшой coupling: она вообще не в одном классе не прописана, все происходит совершенно прозрачно и определяется одной аннотацией на package уровне
В некоторых случаях lazy не спасет да. Пользуюсь трансформаторами при наличии времени, все таки это сложнее и на все потребности бомбить избыточные классы как-то не всегда есть желание, иногда приходится идти на компромиссы. Теперь я убежден что вы шарите :)

Как я уже писал, для меня это статья о @XmlAccessorFactory, а не о lazy связях, так как XmlAccessorFactory мне раньше использовать не приходилось :)
Тоже покритикую чуток автора за штамп «антипаттерн» на идее DTO. Представленная задача прямо-таки напрашивается на два разных представления данных: CompanyAndSuppliersDTO и CompanyAndCustomersDTO, где в первом случает отсутствует поле customers а во втором отсутствует поле suppliers.

Использовать вместо этого Entity, да еще с зануленными полями — вот это действительно антипаттерн, от которого за версту разит NPE.

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

В вышеупомянутой задаче он бы залил содержимое Company с одинаковым успехом и в подмножество полей CompanyAndSuppliersDTO и в подмножество полей CompanyAndCustomersDTO. Отсутствие определенных полей в целевой структуре данных Dozer по-умолчанию принимает как данное и попросту игнорирует.

Т.е. на выходе имеем два представления данных, четко заточенных под получателя, без занулленых полей, без дополнительной конфигурации (в данной задаче Dozer обо всем догадается сам)
Хочу сказать «спасибо» свом критикам, благодяря им я понял свою главную ошибку — не так преподнес статью. У читателей складывается впечатление, что я изобрел эту AccessorFactory, а на всем остальном (пункты 1-5) поставил крест. Конечно же, это не так. Все определяется конкретной ситуацией. В статье нарочно приведен пример, когда использование AccessorFactory оправдано.
Вот вы говорите: давайте сделаем 2 DTO: CompanyAndSuppliersDTO, CompanyAndCustomersDTO. Да давайте, я сам так неоднократно делал: получим помимо прочего 2 DTO + DAO c двумя методами (по одному на DTO). Если еще что-нибудь понадобится (к примеру, появятся новые ленивые колекции), надо будет добавлять новые DTO и методы соответственно.
С AccessorFactory: DAO (с одним методом) + сама AccessorFactory (неограниченно применяемая на все lazyinit-случаи, который в будущем могут возникнуть)
Можно возразить — а мне надо обязательно DTO, я там дополнительные вещи какие-то делаю. Пожалуйста, используйте DTO — оно прекрасно может жить совместно с этой AccessorFactory (она вообще совместима с любыми решениями 1-5), более того, пакет с DTO можно не маркировать @XmlAccessorFactory — и она не будет применяться.

Далее, хочу отметить, что я не зануляю поля у самой entity, я подсовываю сериализатору null вместо полей, которые мне не нравятся, тут все-таки есть разница. Ошибок не будет, если у сериализатора это поле не помечено required. Если помечено — нужно подсовывать что надо, для этого есть все возможности.

Ну и самый скользкий вопрос во всей статье, на который справедливо обращено внимание — я назвал DTO антипаттерном. Каюсь, погорячился. Вопрос «DTO — pattern or antipattern?» существует давно и, боюсь, не нам с вами на него ответить. Я воспринимаю DTO, как средство без которого иногода не обойтись, но с обоснованиями. В своем примере я не вижу причины, почему использование DTO явно выгодней, кроме того, что это стандартный способ к которому все привыкли.
Вот вы говорите: давайте сделаем 2 DTO: CompanyAndSuppliersDTO, CompanyAndCustomersDTO. Да давайте, я сам так неоднократно делал: получим помимо прочего 2 DTO + DAO c двумя методами (по одному на DTO). Если еще что-нибудь понадобится (к примеру, появятся новые ленивые колекции), надо будет добавлять новые DTO и методы соответственно.

Не совсем так.

У вас остается 1 ДАО метод — Company getCompany(). Он дергается из сервисных методов:
@Transactional
CompanyAndSuppliersDTO getCompanyAndSuppliersDTO()

@Transactional
CompanyAndCustomersDTO getCompanyAndCompanyDTO()


А уже эти ДТО имеют соответствующие поля, заполняются там и обвешаны JAXB аннотациями.

DAO уровень ответственнен за доступ к хранилищу. А не за сериализацию, но у вас сериализация именно там (аннотациями навешана). Model — это то с чем работает DAO уровень и что он отдает. То куда он отдает — это последнее место где заканчивается хибернейт контекст и уровень доступа к данным. Дальше ответственность уровня презентации, где и падает LazyInitializationException, потому что ваша модель попрежнему proxy, созданная хибернейтом, но уже не в контексте. И вы пытаетесь пофиксать именно это поведение, хотя необходимо просто перенести сериализацию в правильное место. Осуществить это можно либо переложив данные в POJO этого уровня (можно еще депроксировать Company объект, но это тоже хак)
Ну вот опять мы приехали к частной spring'овой реализации. Да, вы правы, в случае spring так и будет — @service, @transactional, все дела. Мне было важно показать не где эти методы (и DTO) будут, а что они будут вообще и их число будет расти. Я прекраcно понял вашу позицию — вы сторонник классического разделения логики приложения по слоям и не терпите отступлений и экспериментов. Вы не пропускаете lazyinit выше service c помощью ResultTransformer'ов (или Dozer) и считаете, что это едиственно верно и подходит по концепции разбиения на уровни (domain, dao, service...). Но ведь это уровни созданы для вас, а не вы для уровней. Никто не заставляет обязательно следовать их букве и, если я вижу способ сократить объем кода но слегка нарушив концепции без видимых последствий, я его сокращу. И с этой точки зрения нет причин считать что Hibernate.isIniialized более или менее косячен, чем ResultTransformer — они решают одну и ту же проблему. Вопрос, который мы тут начали обсуждать, гораздо шире чем тема статьи. Это вопрос: творчество и малый объем кода vs. подходы, проверенные временем. Хотя, может и я однажды пойму, что в моей работе нет места для творчества и что все следует делать «как надо».
Чем хорош пример с AccessorFactory, это тем, что он замечательно сосуществует со стандартной концепцией и нет нужды чем-то жертвовать. От DTO никто не отказывается, я об этом в предыдущем комменте написал. Эх, вообще тяжело комментировать куски, вырванные из контекста…
Но ведь это уровни созданы для вас, а не вы для уровней. Никто не заставляет обязательно следовать их букве

Именно, а созданы они для избегания проблем с миксованием ответственностей, которые порождают LazyInitException.

Основной мой concern, что вы описали проблему, как боремся с LazyInitException при сериализации Hibernate объектов и предложили решение. Оно для меня выглядит как «у меня переполняется ванна, давайте будем вычерпывать оттуда воду не ведром, а насосом», а я вам советую «вытащить пробку из слива».

Но в любом случае, спасибо за дискуссию. Приятно поговорить на такие темы, чем копирайты обсудждать :)

как затрахала эта кнопка с ответом, что постоянно по ней промахиваюсь. Извините.
А в вашей аналогии что-то есть:)
И вам спасибо.
А где у вас создается и закрывается Hibernate Session?
Обычно скоуп сессии биндится к скоупу HTTP реквеста, и проблемы с LazyInit вылезают только в случае, когда мы отдаем частично не инициализированный объект на какую-то асинхронную обработку. Вы там OpenSessionInView в тексте статьи сразу отбросили, мне интересно почему (на то что там оба lazy поля дернутся, если юзать автоматический сериализатор, давайте пока забьем — это можно решить иначе).
Я его отбросил по той самой причине, на которую вы хотите забить. А так у меня к OpenSessionInView нет претензий.
Что касается сессии

@Service
public class CompanyService {	
	@Autowired
	private CompanyDAO companyDAO;
	
	@Transactional
	public Company getCompany(String fetchProfile) {
		return companyDAO.getCompany(fetchProfile);
	}
}

@Transactional с помощью spring AOP делает мне session.beginTransaction() и session.commit() и session.close(), так что по сути все тут. На вопрос, завязана ли в моей реализации сессия на scope HTTP request — ответ нет. С Hibernate я работаю на spring, а там таких реализаций мне видеть не довелось, если честно. У меня, как привило, время жизни сессии в 99% случаев меньше времени жизни HttpRequest

@Controller
@RequestMapping("rest/company")
public class CompanyController {	
	@Autowired
	private CompanyService companyService;
	
	@RequestMapping(value = "/{respType:customers|suppliers}", method = RequestMethod.GET)
	public void getData(@PathVariable String respType, Model model) {
		String fetchProfile = ("customers".equals(respType)) ? "companyWithCustomers" : "companyWithSuppliers";
		model.addAttribute(companyService.getCompany(fetchProfile));
	}
}
В общем, проверил я это дело. Ответ больше адресован skywatcher, который тоже заинтересовался вопросом. Действительно, сейчас по умолчанию управляемая спрингом hibernate сессия биндится к транзакции — JPA-style такой пошел. Если хочешь поведение OpenSessionInView — надо руками прописывать соответствующий сервлетный фильтр, который идет в составе спринга.

Итого, как бы я лично ответил на задачу, поставленную в посте:
1. Если веб-приложение использует pull-технику в шаблонах, то использовать OpenSessionInView и сериализацию во View или Controller.
2. Если веб-приложение использует push-технику в шаблонах, то использовать дефолтное поведение сессий, а сериализацию можно маркировать аннотациями на уровне обязательных DTO.

(Pull это когда шаблоны сами «дергают» методы на POJO, Push это когда мы сами обязаны напихать данные для показа «куда-то», откуда они потом будут вытащены в шаблоне)

Т.е. ни в одном случае серилизации на уровне доменных объектов нет, на эту тему есть отличное обсуждение выше. Ни разу не хочу при этом как-то принизить работу автора поста, но так уж вышло что комменты в этот раз ценнее.
Товарищ автор, по поводу пункта 3, кстати говоря, я не совсем понял — а чем не подходит вариант открыть сессию по-шире? например в каком-то интерцепторе. Т.е. открывать сессию скажем пер-реквест?

И как понять «вытянутся лишние данные»? Лишние данные вытянутся если сам их вытянешь ручками.

Простите меня, мало просвященного и объясните в чем я не прав, если не прав, буду благодарен.
P.S. прочитал пост выше над моим. Почему у вас в 99% случаев Hibernate сессия живет меньше риквеста? Это было реальной проблемой или же это было premature optimization? Если первое, то интересно узнать кейс. Если второе, то тоже интересно почему.
Сделать время жизни сессии меньше времени жизни реквеста не было самоцелью и ничего специально для этого не делалось. Просто это вытекает из из того кода, который я привел. Прокомментирую:

@RequestMapping(value = "/{respType:customers|suppliers}", method = RequestMethod.GET)
public void getXML(@PathVariable String respType, Model model) {
// Грубо говоря, этот метод определяет собой границы жизни HttpServleеRequest/Response, вернее то 
// место, где мы можем до них добраться, сделав inject в один из параметров, например.
	String fetchProfile = ("customers".equals(respType)) ? "companyWithCustomers" : "companyWithSuppliers";
	model.addAttribute(
           // Тут сессии еще не было
           companyService.getCompany(fetchProfile)
           // А тут она уже все сделала и закрылась
        );
// Дальше Spring положит что надо в response и отправит его
}
Я маловато работал с SpringMVC и REST сервисами. В Strust2 у нас делается так(хотя c XML/REST мы и не работаем):
1). В интерцепторе(на каждый запрос) открывается Hibernate транзакция.
2). Конвертер типов достает hibernate'ом нужную нам сущность по ID.
3). дальше мы внутри контроллера/JSP страницы делаем с этой сущностью что хотим — в том числе итерируемся по lazy коллекциям и т.п.
4). В интерцепторе из пункта 1). коммитим транзакцию после запроса

Такую параллель нельзя провести на Spring?
Тот пример, который я привел, он для spring самый стандартный, проповедуется в литературе, соответствует разбиению логики приожения на уровни, о которых тут так много говорилось выше. Что каксается вашего подхода, то я не виду причин, почему его нельзя написать на spring (ну может там будет побольше кода и меньше спринговых плюшек).
Популярный session-per-request pattern, одни из вариантов рекомендуемых вместо session-per-request анти паттерна. community.jboss.org/wiki/SessionsAndTransactions и community.jboss.org/wiki/OpenSessionInView.

Это все классно конечно, но мне кажется что статья писалась не совсем об этом.
* вместо session-per-operation конечно же *
Я правильно понял, что session-per operation = anti pattern, a session per request = pattern?
Sign up to leave a comment.

Articles