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

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

увидим ошибку:org.hibernate.LazyInitializationException

А вся проблема в том, что надо правильно проектировать архитектуру. И не пренебрегать явным разделением инфраструктурных и бизнес-сервисов.

Реализация остальных сервисов не важна в рамках статьи

Только при условии, что это исключительно сервисы бизнес-логики. Что в свете предложенных решений

Указать аннотацию @Transactional

и

Указать необходимость загрузки поля непосредственно в методе репозитория

очевидно, не так. Т.к. в примерах налицо нарушение основополагающего принципа всей слоёной архитектуры: "Нельзя отдавать внешнему слою модель из внутреннего".

как минимум, могут появиться проблемы с ее [транзакции] изоляцией.

Вообще непонятно в чём проблема с изоляцией транзакций, можете привести пример?

переопределяем поведение методов загрузки

В результате код

Organization organization = organizationService.getById(123L);
organization.getHead();
organization.getEmployees();
organization.getCodes();

Будет открывать как минимум 4 (четыре!!!) разные транзакции, что в конкуррентной среде приведёт к получению несогласованных данных.

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

А вся проблема в том, что надо правильно проектировать архитектуру. И не пренебрегать явным разделением инфраструктурных и бизнес-сервисов.

Это было указано лишь для того, что бы показать, что будет, если мы попытаемся использовать не инициализированное поле.

очевидно, не так. Т.к. в примерах налицо нарушение основополагающего принципа всей слоёной архитектуры: "Нельзя отдавать внешнему слою модель из внутреннего".

«Не так» - а как? Что то я не понял этой части комментария. Что вы предлагаете? Есть другие, более лучшие способы работы с ленивым полем?

Вообще непонятно в чём проблема с изоляцией транзакций, можете привести пример?

Чем длиннее транзакция, тем больше вероятность грязного чтения/записи данных. Если между загрузкой самого объекта и загрузкой ленивого поля (которых может быть несколько) будет некая пауза (которая может произойти даже в результате работы сборщика мусора), мы можем получить несогласованные данные при загрузке ленивых полей. Или надо более тонко управлять транзакциями, а не просто воткнуть аннотацию.

В результате код будет открывать как минимум 4 (четыре!!!) разные транзакции, что в конкуррентной среде приведёт к получению несогласованных данных.

Потому в статье и написано, что код «можно адаптировать» под работу с локальным хранилищем. Во-первых, такой код явно должен быть аннотирован @Transactional.  Во-вторых – я не даром написал этот подход именно при хранении данных во внешнем источнике. При чем, наиболее реальный смысл он имеет только при использовании кэша – иначе, в большинстве случаев, «жирный» запрос будет выгоднее. Без кэша при работе с локальной бд всегда будет удобнее и выгоднее использовать уже существующие средства JPA (в т.ч. описанные в статье), чем подгружать поля отдельными запросами. Но если имеет место быть кэш, то в некоторых ситуациях можно подумать.

Ну и про точечные методы – я написал их проблему: количество.

Подводя итог, смысл статьи – не предложить везде использовать последний подход. Смысл статьи – показать способы работы с ленивыми полями. Даже указал в конце, что применять последний подход или нет – зависит от ситуации)

«Не так» - а как? Что то я не понял этой части комментария.

"Не так" тут то, что сервису, реализующему бизнес-логику (получение кода организации по её идентификатору), приходится заниматься инфраструктурными делами.

Что вы предлагаете?

Разделять бизнес-логику и инфраструктуру. Методы сервиса не вызываются в вакууме, это всегда какой-либо адаптер для взаимодействия с внешним миром (контроллер, например, или слушатель какой-либо очереди в брокере сообщений). Внутри адаптера должен вызываться инфраструктурный сервис, который и будет заниматься транзакциями и другими настройками окружения с последующий делегацией вызова в бизнесовый сервис, получением от него доменного объекта и конвертацией его в подходящую DTO.

Чем длиннее транзакция, тем больше вероятность грязного чтения/записи данных.

Если грязное чтение так критично, то, очевидно, нужно использовать блокировки и/или соответвтсвующий уровень изоляции. А от транзакции никуда в общем случае не деться по-любому, т.к. схема любой бизнес-операции в самом тупом её виде "Прочитать из БД" -> "Обработать" -> "Сохранить". Причём в случае с JPA последний пункт будет выполнен без явного на то указания, достаточно лишь изменить состояние прочитанной из БД сущности.

реальный смысл он имеет только при использовании кэша

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

про точечные методы – я написал их проблему: количество

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

P.S. Вообще если бы кто-то на заре spring-data не принял в угоду упрощения идиотское решение проаннотировать дефолтную реализацию JpaRepository @Transactional, то и никакой бы проблемы не было. Т.к. методы репозитория падали из-за отсутствия открытой транзакции, заставляя разработчика думать, а не тупо копиравать строчки из step-by-step руководств.

Разделять бизнес-логику и инфраструктуру....

Я боюсь, в моих примерах в принципе нет бизнес логики. Повторюсь, но в данной статье лишь указаны способы инициализации ленивых полей. Что бы это ни было, для того, что бы инициализировать ленивое поле, придется либо указывать @Transactional, либо создавать соответствующий метод в репозитории (или реализовывать более низкоуровневые варианты, это, конечно, тоже имеет место быть).

Если грязное чтение так критично, то, очевидно, нужно использовать блокировки и/или соответвтсвующий уровень изоляции....

Очевидно. Но это не отменяет написанного: длительные транзакции - зло. Мы можем избавиться от грязного чтения путем установки уровня изолированности (того же дефолтного в PostgreSQL READ_COMMITED), но появятся другие проблемы (например, невоспроизводимое чтение). Если мы поставим еще более высокий уровень изолированности - появятся проблемы с производительностью. Поэтому:

  1. Лучше, когда длительных транзакций нет, чем когда они есть.

  2. В статье я указал, что считаю вариант с точечным методом в случае работы с бд предпочтительнее.

  3. Всё остальное зависит от конкретной ситуации.

Кэширование данных - тоже ничего с бизнес-логикой не имеет, этим занимаются инфраструктурные сервисы.

Каким образом отсутствие связи между кэшированием и бизнес-логикой коррелирует с тем, что определенные подходы становятся намного эффективнее, если применяется кэширование?

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

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

P.S. Вообще если бы кто-то на заре spring-data не принял в угоду упрощения идиотское решение проаннотировать дефолтную реализацию JpaRepository @Transactional, то и никакой бы проблемы не было. Т.к. методы репозитория падали из-за отсутствия открытой транзакции, заставляя разработчика думать, а не тупо копировать строчки из step-by-step руководств.

И тогда все разработчики просто помечали бы все методы @Transactional)

Что бы это ни было, для того, что бы инициализировать ленивое поле, придется либо указывать @Transactional, либо создавать соответствующий метод в репозитории

чем плох по вашему вариант с hibernate.enable_lazy_load_no_trans = true? риском постоянного грязного чтения в конкурентной среде? или есть еще подводные камни этого самокостыля от хиба?

Так ведь в этом случае каждое поле грузится в отдельной транзакции. Это хорошо лишь тогда, когда разработчик вообще не хочет думать об архитектуре и действительно бездумно открывает транзакции, или если нет вообще никаких претензий приложения на производительность (что, в общем то, не отменяет первого).

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

Лучше планировать транзакции так, что бы все необходимые поля инициализировались в единственной транзакции.

И тогда все разработчики просто помечали бы все методы @Transactional)

И это было бы хорошо. Явное - лучше неявного.

Когда код надо/не надо пестрит @Transactional-ми, то, если разработчик не полный долбоящер, мысль, что что-то тут не так, должна появиться.

Исключая блобы, поиск обычно дольше выборки. Насчёт кэша - чтобы поле взялось из кэша, туда его кто-то должен положить. Инвалидировать на уровне не только строк, но и полей - плодить сложность там, где выгоды от неё не будет - ибо количество метаданных сравнится с количеством самих данных. В итоге вся затея с ленивой вычиткой полей больше похоже на натягивание абстрактной совы на такой же глобус. Гораздо эффективнее сделать например пару методов - загрузить ключи, референсы и то, что характеризует экземпляр через tostring например, и второй - загрузить полностью. Прибыль от первого будет в основном, если поля целиком покрываются индексом, по которому работает предикат, либо присутствуют в его included-колонках.

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

Вот кейс: существуют некие "заявки". У них есть 15 полей, среди которых отдельными сущностями сидят: создатель, организация, предоставляющая услугу, исполнитель, статус. Создатель/исполнитель/организация довольно крупные объекты.

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

Так вот, по факту, нам нужно вытянуть лишь саму заявку и ее статус. Статус представляет собой сущности в ограниченном количестве (допустим, 10 различных статусов). Они очень редко меняются, а если кто то и поменял название, то ничего страшного, что пользователь некоторое время увидит старое (и вместо статусов можно взять, например, некий тип заявки, или любое "классификационное значение" - да вообще много можно чего придумать).

И в этом случае нам гораздо проще вытащить сначала заявки, потом проверить наличие статусов в кэше, и запросить отсутствующие статусы. При обновлении страницы нам уже не придется дозапрашивать статусы - с большой вероятностью они будут лежать в кэше, и мы будем справляться с обновлением в среднем за 1 запрос, при этом намного меньше нагружая как сеть, так и сервис, предоставляющий данные.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории