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

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

    Выглядит это все примерно так
    public class User {
        ...
    
        @OneToMany(fetch = FetchType.LAZY)
        private List<Phone> phones = new ArrayList<Phone>();
    
        public List<Phone> getPhones() {
            return phones;
        }
    }
    
    public class Phone {
        ...
    }
    
    public class CellPhone extends Phone {
        ...
    }
    
    public class SatellitePhone extends Phone {
        ...
    }
    


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

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


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

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

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

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

    Пример реализации JpaDao
    public abstract class JpaDao<ENTITY extends BaseEntity> {
        ...
    
        private ENTITY unproxy(ENTITY entity) {
            if (entity != null) {
                if (entity instanceof HibernateProxy) {
                    Hibernate.initialize(entity);
                    entity = (ENTITY) ((HibernateProxy) entity).getHibernateLazyInitializer().getImplementation();
                }
            }
            return entity;
        }
    
        private List<ENTITY> unproxy(List<ENTITY> entities) {
    
            boolean hasProxy = false;
            for (ENTITY entity : entities) {
                if (entity instanceof HibernateProxy) {
                    hasProxy = true;
                    break;
                }
            }
    
            if (hasProxy) {
                List<ENTITY> unproxiedEntities = new LinkedList<ENTITY>();
                for (ENTITY entity : entities) {
                    unproxiedEntities.add(unproxy(entity));
                }
    
                return unproxiedEntities;
            }
    
            return entities;
        }
    
        ...
    
        public List<ENTITY> findAll() {
            return unproxy(getEntityManager().createQuery("from " + entityClass.getName(), entityClass).getResultList());
        }
    
        ...
    }
    


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

    Пример реализации геттера списка телефонов у пользователя
    public class User {
        ...
    
        @OneToMany(fetch = FetchType.LAZY)
        private List<Phone> phones = new ArrayList<Phone>();
    
        public List<Phone> getPhones() {
            return ConverterUtil.unproxyList(phones);
        }
    }
    
    public class ConverterUtil {
        ...
    
        public static <T> T unproxy(T entity) {
            if (entity == null) {
                return null;
            }
    
            Hibernate.initialize(entity);
            if (entity instanceof HibernateProxy) {
                entity = (T) ((HibernateProxy) entity).getHibernateLazyInitializer().getImplementation();
            }
            return entity;
        }
    
        public static <T> List<T> unproxyList(List<T> list) {
            boolean hasProxy = false;
            for (T entity : list) {
                if (entity instanceof HibernateProxy) {
                    hasProxy = true;
                    break;
                }
            }
    
            if (hasProxy) {
                LinkedList<T> result = new LinkedList<T>();
    
                for (T entity : list) {
                    if (entity instanceof HibernateProxy) {
                        result.add(ConverterUtil.unproxy(entity));
                    } else {
                        result.add(entity);
                    }
                }
    
                list.clear();
                list.addAll(result);
            }
    
            return list;
        }
    }
    


    В данной статье я продемонстрировал способ использования ленивой загрузки Hibernate при использовании списков, содержащих объекты разных типов (с одним родителем), используемый в моей команде. Надеюсь этот пример поможет кому-нибудь в аналогичной ситуации. Если вы знаете более оптимальный/красивый способ победить эту проблему, буду рад добавить его в статью.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 35

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

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

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

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

                          Only users with full accounts can post comments. Log in, please.