Hibernate использует прокси-объекты для реализации ленивой загрузки (lazy load) связей "к-одному". Их также можно использовать для улучшения производительности некоторых операций записи. 

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

11:17:03,697  INFO TestSample:80 - com.thorben.janssen.sample.model.ChessPlayer$HibernateProxy$W2cPgEkQ

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

Как Hibernate генерирует прокси

Hibernate генерирует класс для прокси как подкласс вашей сущности. Начиная с Hibernate 5.3 для его генерации используется Byte Buddy. В более старых версиях использовался Javassist и CGLIB.

Сгенерированный прокси перехватывает все вызовы методов и проверяет, был ли инициализирован проксируемый объект. При необходимости перед выполнением перехваченного метода выполняется запрос к базе данных для инициализации сущности. Если это происходит без активной Hibernate Session, то бросается исключение LazyInitializationException.

Как получить прокси-объект

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

Проксированные lazy-ассоциации "к-одному"

По умолчанию значение FetchType всех ассоциаций "к-одному" равно EAGER. Это означает, что Hibernate должен получить связанный объект сразу при загрузке сущности. Вы можете изменить это поведение, установив в аннотации @OneToOne или @ManyToOne значение FetchType.LAZY для атрибута fetch.

@Entity
public class ChessGame {
 
    @ManyToOne(fetch = FetchType.LAZY)
    private ChessPlayer playerWhite;
 
    @ManyToOne(fetch = FetchType.LAZY)
    private ChessPlayer playerBlack;
     
    ...
}

Используя ленивую загрузку для ассоциаций "к-одному" вы создаете проблему вашему persistence-провайдеру. Теперь он должен придумать, как узнать, что ваш код хочет использовать ассоциацию и получить связанный объект из базы данных. Для ассоциаций "ко-многим" Hibernate решает эту проблему, инициализируя атрибут собственными реализациями коллекций. Но это не работает для ассоциаций "к-одному". Hibernate не требует от ваших сущностей реализации каких-либо интерфейсов, которые потом он мог бы имплементировать. Тогда остается два варианта:

  1. Добавить некоторый код в get-метод 

или

  1. Сгенерировать прокси-класс, являющийся подклассом вашей сущности.

Первый вариант требует изменения байткода. Это тема для другой статьи, подробнее об этом я расскажу в онлайн-тренинге Hibernate Performance Tuning. В этой статье мы сконцентрируемся на генерации прокси.

Получение прокси для инициализации ассоциации

Можно получить прокси для сущности, вызвав метод getReference у EntityManager или Hibernate Session. В результате возвращается объект, который можно использовать для ассоциации "к-одному" при создании или изменении сущности.

// get a proxy
ChessTournament chessTournament = em.getReference(ChessTournament.class, tournamentId);
 
ChessGame chessGame = new ChessGame();
chessGame.setRound(2);
chessGame.setTournament(chessTournament);
em.persist(chessGame);

Как вы можете видеть из нижеприведенного лога Hibernate, вызов метода getReference не инициирует запрос к базе данных. Hibernate создает экземпляр прокси-объекта и устанавливает значение только для первичного ключа. Выполнение запроса откладывается до вызова геттера или сеттера для любого поля, не являющегося первичным ключом.

11:11:53,506 DEBUG SQL:144 - select nextval ('hibernate_sequence')
11:11:53,509 DEBUG SQL:144 - insert into ChessGame (chessTournament_id, date, playerBlack_id, playerWhite_id, round, version, id) values (?, ?, ?, ?, ?, ?, ?)

Как определить прокси-объект

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

11:19:54,433 ERROR TestSample:142 - org.hibernate.LazyInitializationException: could not initialize proxy [com.thorben.janssen.sample.model.ChessPlayer#101] - no Session

Для определения является объект прокси или нет проверьте, реализует ли он HibernateProxy. HibernateProxy — это один из маркерных интерфейсов Hibernate. Если вы также хотите проверить, инициализирован ли прокси, то можно использовать статический метод isInitialized класса Hibernate.

В примере ниже я использую обе проверки для атрибута playerWhite, который представляет собой lazy-ассоциацию "к-одному".

ChessGame chessGame = em.find(ChessGame.class, this.chessGame.getId());
 
assertThat(chessGame.getPlayerWhite()).isInstanceOf(HibernateProxy.class);
assertFalse(Hibernate.isInitialized(chessGame.getPlayerWhite()));

Как инициализировать прокси

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

ChessGame chessGame = em.find(ChessGame.class, this.chessGame.getId());
log.info(chessGame.getPlayerWhite().getClass().getName());
 
log.info("==== Test Assertions ====");
assertThat(chessGame.getPlayerWhite().getFirstName()).isEqualTo(player1.getFirstName());

Атрибут playerWhite сущности ChessGame моделирует lazy-ассоциацию "к-одному". Как видно из лога, Hibernate инициализировал его сгенерированным прокси-объектом. И когда позднее у этого объекта вызывается метод getFirstName(), Hibernate выполняет дополнительный SQL-запрос для инициализации прокси.

11:49:41,984 DEBUG SQL:144 - select chessgame0_.id as id1_0_0_, chessgame0_.chessTournament_id as chesstou5_0_0_, chessgame0_.date as date2_0_0_, chessgame0_.playerBlack_id as playerbl6_0_0_, chessgame0_.playerWhite_id as playerwh7_0_0_, chessgame0_.round as round3_0_0_, chessgame0_.version as version4_0_0_ from ChessGame chessgame0_ where chessgame0_.id=?
11:49:42,006  INFO TestSample:122 - com.thorben.janssen.sample.model.ChessPlayer$HibernateProxy$dWs3SOcI
11:49:42,006  INFO TestSample:126 - ==== Test Assertions ====
11:49:42,006 DEBUG SQL:144 - select chessplaye0_.id as id1_1_0_, chessplaye0_.birthDate as birthdat2_1_0_, chessplaye0_.firstName as firstnam3_1_0_, chessplaye0_.lastName as lastname4_1_0_, chessplaye0_.version as version5_1_0_, gameswhite1_.playerWhite_id as playerwh7_0_1_, gameswhite1_.id as id1_0_1_, gameswhite1_.id as id1_0_2_, gameswhite1_.chessTournament_id as chesstou5_0_2_, gameswhite1_.date as date2_0_2_, gameswhite1_.playerBlack_id as playerbl6_0_2_, gameswhite1_.playerWhite_id as playerwh7_0_2_, gameswhite1_.round as round3_0_2_, gameswhite1_.version as version4_0_2_ from ChessPlayer chessplaye0_ left outer join ChessGame gameswhite1_ on chessplaye0_.id=gameswhite1_.playerWhite_id where chessplaye0_.id=?

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

Hibernate.initialize(chessGame.getPlayerWhite());

Как получить исходную сущность из прокси

До Hibernate 5.2.10 получение исходного объекта из прокси требовало небольшого количества кода. Сначала нужно было привести объект к HibernateProxy, чтобы получить доступ к LazyInitializer, а затем использовать его для получения объекта.

ChessPlayer playerWhite = chessGame.getPlayerWhite();
 
ChessPlayer unproxiedPlayer;
if(playerWhite instanceof HibernateProxy) {
    HibernateProxy hibernateProxy = (HibernateProxy) playerWhite;
    LazyInitializer initializer =
        hibernateProxy.getHibernateLazyInitializer();
    unproxiedPlayer = (ChessPlayer) initializer.getImplementation();
}

С версии 5.2.10 ту же функциональность предоставляет статический метод unproxy класса Hibernate, что значительно упрощает работу.

ChessPlayer unproxiedPlayer = Hibernate.unproxy(playerWhite, ChessPlayer.class);

Проблема при работе с прокси

Как я объяснял ранее, Hibernate генерирует прокси-объект, который является подклассом вашего класса сущности. Это может стать проблемой, если ваша ассоциация "к-одному" ссылается на суперкласс в иерархии наследования. В этом случае Hibernate генерирует другой подкласс этого суперкласса, и вы не сможете легко привести его к своему подклассу.

Давайте рассмотрим это на примере. Сущность ChessGame определяет lazy-ассоциацию к сущности ChessTournament. А объект ChessSwissTournament является подклассом объекта ChessTournament.

Когда я загружаю объект ChessGame, ссылающийся на ChessSwissTournament, Hibernate инициализирует атрибут tournament с помощью прокси-объекта, который является подклассом сущности ChessTournament и реализует HibernateProxy. Но это не экземпляр ChessSwissTournament.

ChessGame chessGame = em.find(ChessGame.class, newChessGame.getId());
assertThat(chessGame.getTournament()).isInstanceOf(ChessTournament.class);
assertThat(chessGame.getTournament()).isNotInstanceOf(ChessSwissTournament.class);
assertThat(chessGame.getTournament()).isInstanceOf(HibernateProxy.class);

Резюме

Hibernate использует сгенерированные прокси-классы для поддержки ленивой загрузки ассоциаций "к-одному", и вы можете использовать их для инициализации ассоциаций к другим сущностям. Как только вы вызываете геттер или сеттер для поля, не являющегося первичным ключом, Hibernate выполняет SQL-запрос для получения связанной сущности.

Прокси-класс является подклассом вашей сущности и реализует интерфейс HibernateProxy. Это позволяет вам использовать прокси-объект почти так же, как и объект сущности, но с двумя ограничениями:

  1. Если вы хотите инициализировать прокси-объект, вам нужно делать это с активной сессией Hibernate Session. В противном случае Hibernate выбросит исключение LazyInitializationException.

  2. Если у вас есть lazy-ассоциация "к-одному" к суперклассу в иерархии наследования, то вы не сможете привести прокси-объект в какому-либо из ваших подклассов. Сначала вам нужно получить оригинальную сущность через unproxy.


Материал подготовлен в рамках курса «Java Developer. Professional».

Всех желающих приглашаем на открытый урок «Telegram bot для получения курса валют». На занятии создадим пользовательский интерфейс, для этого мы сделаем Telegram bot-а. Через него конечный пользователь сможет получать курс валют.
>> РЕГИСТРАЦИЯ