Что такое ленивая загрузка java

Hibernate: ленивая загрузка, наследование и instanceof

Рассмотрим, в качестве примера, следующую ситуацию. У нас имеется класс User с полями, описывающими пользователя. Имеется класс Phone, который является родительским для классов CellPhone и SatellitePhone. В классе User есть поле содержащее список телефонов пользователя. В целях уменьшения нагрузки на БД мы сделали этот список «ленивым». Он будет загружаться только по требованию.

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

Давайте разберемся почему так происходит. Основная проблема заключается в том, что Hibernate — не экстрасенс и не может знать заранее (не выполнив запросы к БД) какого типа объекты содержатся в списке. В соответствии с этим создает список, содержащий proxy-объекты, унаследованные от Phone.

Когда наша команда в первый раз столкнулась с данной проблемой мы немного изучили данный вопрос и поняли, что придется делать «костыль». Ошибка возникала в сервисном методе где нужно было точно знать с каким из дочерних классов мы имеем дело. Мы прямо перед этой проверкой внедрили другую: если объект является proxy-объектом, то он инициализируется. После чего благополучно забыли эту неприятную историю.

Со временем проект все рос, бизнес-логика усложнялась. И вот настал момент, когда подобных костылей стало уже слишком много (мы поняли, что так дело не пойдет на третьем или четвертом костыле). Причем данная проблема стала возникать не только при запросе у одного объекта ленивого списка других объектов, но и при прямом запросе из базы данных списка объектов. Отказываться от ленивой загрузки очень не хотелось т.к. база у нас и так сильно нагружена. Мы решили больше не перемешивать архитектурные слои приложения и создать что-нибудь более универсальное.

Схема нашего приложения

В данной схеме запросами к БД занимается DAO слой. Он состоит из 1 абстрактного класса JpaDao в котором определены все базовые методы по работе с базой данных. И множества классов — его наследников, каждый из которых в конечном итоге использует методы базового класса. Итак, как мы побороли проблему с прямым запросом списка объектов разных типов с общим родителем? Мы создали в классе JpaDao методы для инициализации одного прокси-объекта и инициализации списка прокси-объектов. При каждом запросе списка объектов из БД этот список проходит инициализацию (Мы сознательно пошли на такой шаг т.к. если мы запрашиваем какой-то список объектов в нашем приложении — то почти всегда он нужен полностью проинициализированным).

С решением первой проблемы все получилось не так гладко. Вышеописанный способ не подходит так как ленивой загрузкой занимается непосредственно Hibernate. И мы пошли на небольшую уступку. Во всех объектах, содержащих ленивые списки разных типов объектов с одним родителем (например, User со списком Phone) мы переопределили геттеры для этих списков. Пока списки не запрашиваются — все в порядке. Объект содержит только прокси-список и не выполняются лишние запросы. При запросе списка происходит его инициализация.

Источник

Hibernate: использование lazy initialization в разработке клиент-серверного приложения

При проектировании доменов приложения, разрабатываемого с использованием Hibernate, разработчику необходимо сделать выбор: инициализировать ли свойства домена, соответствующие коллекциям связанных доменов, сразу (FetchType=EAGER) или же делать это только при обращении к ним (FetchType=LAZY). На самом деле в случае, когда предметная область имеет сколь-либо сложную структуру связей между объектами, выбор уже сделан – загружать полбазы ради одного объекта, как это было бы при FetchType=EAGER, мягко говоря, неразумно. Поэтому ленивая инициализация в случае коллекций есть наиболее предпочтительная стратегия инициализации связанных объектов.

Однако, не всё так просто. Ленивая инициализация реализуется за счёт создания прокси-объекта с помощью JDK Dynamic Proxy или библиотеки CGLIB. В обоих случаях проксирование соответствующих get-методов сводится к обращению к сессии Hibernate для получения необходимых данных. Последнее же в свою очередь означает, что обращение к ленивым свойствам объекта может быть осуществлено только при наличии сессии Hibernate. В противном случае, попытка получить свойство объекта приведёт к незабвенному исключению «LazyInitializationException: could not initialize proxy — the owning Session was closed».

Понятно, что иметь открытую сессию под рукой можно далеко не всегда, что доставляет некоторые неудобства. Так, например, для того, что бы использовать домены с ленивой инициализацией в шаблонах MVC-приложения придётся прибегнуть к методике «OpenSessionInView», суть которой сводится к созданию фильтра, обеспечивающего открытие сессии в начале обработки запроса и её закрытие в конце.

Но, что делать, если домены с загруженными данными нужно использовать вне сессии Hibernate? Например, в случае клиент-серверной архитектуры, когда серверу необходимо передать домены клиенту? Разумеется, что об открытие сессии на стороне клиента не может идти и речи, хотя бы потому, что в общем случае о БД ему ничего не известно. Единственным выходом из ситуации, на мой взгляд, будет «депроксирование» объектов доменов и инициализация их необходимыми данными перед передачей от сервера клиенту.

Читайте также:  Что такое одна подсеть

При таком раскладе слой бизнес логики может спокойно работать с прокси-объектами доменов в рамках сессии Hibernate. Роль слоя служб при этом сводится получению необходимых данных от слоя бизнес-логики и компоновки данных для клиента: создание DTO-объектов на базе классов доменов с помощью копирования определённой глубины и детализации.

Вопрос остаётся только в том, как провести это «депроксирование». В принципе это можно сделать с помощью самого Hibernate’a:

public static T initializeAndUnproxy(T entity) <
if (entity == null ) <
throw new
NullPointerException( «Entity passed for initialization is null» );
>

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

Решением в данной ситуации может послужить создание небольшого класса-утилиты, который будет выполнять депроксирование домена путём создания нового объекта того же класса и инициализации свойств, соответствующих базовым классам Hibernate’a. Все остальные свойства объекта будут инициализироваться слоем служб по мере необходимости.

Я не берусь, утверждать, что данный подход является оптимальным и/или единственно верным. Если у вас есть предложения, как решить проблему иначе, — буду рад их услышать.

Источник

Java Blog

Spring Boot: ленивая инициализация (Lazy Initialization)

SpringApplication позволяет лениво инициализировать приложение. Когда ленивая инициализация включена, компоненты (beans) создаются по мере необходимости, а не во время запуска приложения. В результате включение ленивой инициализации может сократить время, необходимое для запуска приложения. В веб-приложении включение ленивой инициализации приведет к тому, что многие веб-компоненты (beans) не будут инициализированы до получения HTTP-запроса.

Недостатком ленивой инициализации является то, что она может задержать обнаружение проблемы с приложением. Если неправильно сконфигурированный компонент (bean) инициализируется лениво, сбой больше не будет возникать во время запуска, и проблема станет очевидной только после инициализации компонента (bean). Необходимо также позаботиться о том, чтобы JVM имела достаточно памяти для размещения всех компонентов (beans) приложения, а не только тех, которые инициализируются во время запуска. По этим причинам ленивая инициализация не включена по умолчанию, и рекомендуется выполнить точную настройку размера кучи JVM перед включением ленивой инициализации.

Ленивая инициализация может быть включена программно с помощью метода lazyInitialization в SpringApplicationBuilder или метода setLazyInitialization в SpringApplication. Кроме того, ее можно включить с помощью свойства spring.main.lazy-initialization, как показано в следующем примере:

Если вы хотите отключить ленивую инициализацию для определенных компонентов (beans) при использовании ленивой инициализации для остальной части приложения, вы можете явно установить для их атрибута lazy значение false, используя аннотацию @Lazy(false).

Источник

Что такое ленивая загрузка в Hibernate?

Что такое ленивая загрузка в Java? Я не понимаю процесс. Кто-нибудь может помочь мне понять процесс отложенной загрузки?

Скажем, у вас есть родитель, и у этого родителя есть коллекция детей. Hibernate теперь может «лениво загружать» дочерние элементы, что означает, что он фактически не загружает все дочерние элементы при загрузке родительского элемента. Вместо этого он загружает их по запросу. Вы можете запросить это явно или, что гораздо чаще, hibernate будет загружать их автоматически при попытке доступа к дочернему элементу.

Lazy-loading может помочь значительно улучшить производительность, так как часто дети вам не нужны и поэтому они не будут загружаться.

Также остерегайтесь проблемы n + 1. Hibernate не будет загружать все дочерние элементы при доступе к коллекции. Вместо этого он загрузит каждого ребенка в отдельности. При выполнении итерации по коллекции это вызывает запрос для каждого дочернего элемента. Чтобы избежать этого, вы можете обмануть hibernate для загрузки всех дочерних элементов одновременно, например, вызвав parent.getChildren (). Size ().

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

Объект, который не содержит всех необходимых вам данных, но знает, как их получить.

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

Это не шаблон, специфичный для доступа к данным и Hibernate, но он особенно полезен в таких областях, и Hibernate поддерживает ленивую загрузку ассоциаций «один ко многим» и ассоциаций «один к одному» и «многие к одному». при определенных условиях. Ленивое взаимодействие обсуждается более подробно в Главе 19 Справочной документации по Hibernate 3.0.

Ленивое извлечение решает, загружать ли дочерние объекты при загрузке родительского объекта. Вы должны сделать эту настройку соответствующего файла отображения hibernate родительского класса. Lazy = true (означает не загружать дочерние элементы). По умолчанию отложенная загрузка дочерних объектов имеет значение true.

Это гарантирует, что дочерние объекты не загружаются, если они явно не вызваны в приложении путем вызова getChild() метода для parent. В этом случае hibernate выдает новый вызов базы данных, чтобы загрузить дочерний getChild() объект, когда он фактически вызывается для родительского объекта.

Читайте также:  какие стихии камней совместимы

Но в некоторых случаях вам нужно загрузить дочерние объекты, когда родительский загружен. Просто сделайте lazy = false, и hibernate загрузит дочерний элемент при загрузке parent из базы данных.

Пример: если у вас есть СТОЛ? EMPLOYEE отображается на объект Employee и содержит набор объектов Address. Родительский класс: класс сотрудников, дочерний класс: адресный класс

Источник

Русские Блоги

java8 Stream Lazy объяснение (принцип неисполнения)

Использование «ленивой» операции типа Stream

Многие операции в коде являются нетерпеливыми, например, когда происходит вызов метода, параметры будут оцениваться немедленно. В целом, использование метода Eager упрощает само кодирование, но использование метода Lazy обычно означает лучшую эффективность.

В этой статье показано, как новые функции в Java 8 позволяют нам писать код Lazy более удобно.

Задержка инициализации

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

Однако при реализации отложенной инициализации объекта важно отметить, что эти подробности реализации не должны предоставляться пользователю, то есть пользователь может использовать объект в соответствии с обычным процессом.

Типичная реализация

Используйте приведенный выше код:

Хотя он может обеспечить создание только одного тяжелого экземпляра, недостаток также очевиден: каждый раз, когда вы вызываете метод getHeavy, вам необходимо вводить область дорогостоящего синхронизированного кода. Фактически, только когда вам необходимо создать экземпляр Heavy в первый раз, вам необходимо обеспечить безопасность потоков. После создания экземпляра нет необходимости использовать синхронизированный для обеспечения безопасности потока.

Используйте лямбда-выражения

Здесь нам нужно использовать интерфейс функции Supplier, который определяет метод get для получения требуемого экземпляра:

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

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

Когда текущие два потока вызывают этот метод, они вызывают метод createAndCacheHeavy, потому что этот метод синхронизирован. Таким образом, первый поток входит в тело метода, а второй поток начинает ждать. В теле метода сначала определяется, является ли текущее значение Heavy экземпляром HeavyInstance. Если нет, тяжелый объект будет заменен экземпляром типа HeavyFactory. Очевидно, что когда первый поток выполняет суждение, тяжелый объект является только экземпляром поставщика, поэтому тяжелый объект будет заменен экземпляром HeavyFactory, а затем будет фактически создан экземпляр Heavy. Когда второй поток входит для выполнения метода, heavy уже является экземпляром HeavyFactory, поэтому он немедленно вернется. Когда третий поток выполняет метод getHeavy, поскольку тяжелый объект в это время уже является экземпляром HeavyFactory, он будет напрямую возвращать требуемый экземпляр, который не имеет ничего общего с методом синхронизации createAndCacheHeavy.

Приведенный выше код фактически реализует облегченный шаблон виртуального прокси (Virtual Proxy Pattern). Убедитесь в правильности ленивой загрузки в различных средах.

Ленивая оценка

Фактически, некоторые места в языке Java применяют концепцию отложенной оценки, например, оценку логических выражений:

В исполнении fn1() || fn2() В это время, когда fn1 () возвращает true, fn2 () не будет выполняться. Аналогично в реализации fn1() && fn2() В это время, когда fn1 () возвращает false, fn2 () не будет выполняться. Это известно как «операция короткого замыкания».

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

Когда в списке параметров есть лямбда-выражения и ссылки на методы, этот тип параметра будет оцениваться компилятором Java только тогда, когда он действительно должен быть использован. Мы можем использовать это для реализации отложенной оценки. Многие методы недавно добавленного типа Stream в Java 8 реализуют отложенную оценку. Например, интерфейс функции Predicate, принятый методом filter, может вызываться не всеми элементами в коллекции. Поэтому мы можем рассмотреть параметры метода, чтобы сформировать функциональный интерфейс для достижения отложенной оценки.

Нетерпеливая оценка

В приведенном выше коде, хотя я хочу использовать операцию короткого замыкания для получения окончательного результата (input1 && input2), уже слишком поздно. При оценке параметров значения input1 и input2 фактически были подтверждены, как видно из выходных данных выше. Этот код будет выполняться не менее 4 секунд, что явно не оптимально.

Дизайн для отложенной оценки

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

После замены функциональным интерфейсом типа поставщика, только когда вызывается его метод get, операция оценки будет фактически выполнена. Тогда приведенная выше операция короткого замыкания имеет смысл. Когда input1.get () возвращает false, input2.get () вообще не будет вызываться:

Читайте также:  ssd 240gb что это

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

Использование потока «ленивый»

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

Промежуточное и Терминальное Управление

Тип Stream имеет два типа методов:

Секрет «ленивости» Stream в том, что каждый раз, когда вы используете Stream, он соединяет несколько промежуточных операций и присоединяет конечную операцию в конце. Такие методы, как map () и filter (), являются промежуточными операциями, и при их вызове немедленно возвращается другой объект Stream. Для таких методов, как redu () и findFirst (), они являются конечными операциями, а реальные операции выполняются при их вызове для получения требуемых значений.

Например, когда нам нужно напечатать первое заглавное имя длины 3:

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

Но реальная ситуация не такова, не забывайте, что Stream очень «ленив», он не будет выполнять никаких дополнительных операций.

Порядок оценки метода

Но на самом деле, только когда вызывается метод findFirst, методы filter и map будут фактически запущены. Фильтр не будет фильтровать всю коллекцию сразу, он будет фильтровать по одному. Если найден элемент, который удовлетворяет условиям, он будет помещен в следующую промежуточную операцию, которая является методом карты. Следовательно, реальный порядок исполнения таков:

\

Вывод консоли такой:

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

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

Чтобы увидеть, что происходит внизу, мы можем разделить вышеуказанные операции над Stream по типу:

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

Создать бесконечную коллекцию

Еще одной особенностью типа Stream является то, что они могут быть неограниченными. Это не то же самое, что типы коллекций. Типы коллекций в Java должны быть ограничены. Причина, по которой Stream может быть бесконечным, также обусловлена ​​характеристикой «лени» Stream.

Например, мы можем использовать тип Stream для выражения последовательности простых чисел. Во-первых, нам нужен инструментальный метод, чтобы определить, является ли число простым числом:

Здесь используется другая особенность IntStream, то есть метод rangeClosed используется для получения объекта IntStream, представляющего определенный диапазон. Затем используется метод noneMatch объекта Stream. Этот метод будет принимать интерфейс функции типа Predicate в качестве параметра. Только когда все элементы в Stream не удовлетворяют Predicate, он возвращает true.

Таким образом, мы можем произвольно указать начальную точку, чтобы получить все простые числа, начиная с этой начальной точки:

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

Так почему же Stream может представлять бесконечную коллекцию? Это также вытекает из «ленивых» характеристик Stream. Stream возвращает только нужные вам элементы, а не всю бесконечную коллекцию одновременно.

В интерфейсе Stream есть статический метод iterate (), который может создать для вас бесконечный объект Stream. Необходимо принять два параметра:

seed представляет начальную точку этой бесконечной последовательности, а UnaryOperator представляет, как получить следующий элемент на основе предыдущего элемента, например, второй элемент в последовательности может быть определен следующим образом: f.apply(seed) 。

Поэтому, основываясь на начальной точке и требуемом количестве простых чисел, мы можем написать следующий код:

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

Вызов метода простых чисел также очень интуитивен:

Источник

Информ портал о технике и не только