Введение
Это вторая статья, связанная с пониманием, поиском и устранением проблемы N+1. Первая статья была про стратегии извлечения данных. Эта статья будет про Hibernate proxy. Понимание концепций стратегии извлечения данных и Hibernate прокси, это ключи к пониманию причин, приводящих к N+1. Подробней про N+1, будет рассказано в следующих статьях.
Данная статья не предполагает подробный разбор всех аспектов связанных с Hibernate proxy. Это верхнеуровневый взгляд на то, что такое Hibernate proxy классы, зачем они нужны, и как связаны с проблемой N+1.
Hibernate прокси, зачем это?
Если коротко, то Hibernate proxy следят за тем, чтоб связанные поля основной сущности были актуальными, если proxy не могут это гарантировать они выбрасывают ошибку.
Чтоб разобраться в вопросе подробней, прежде всего следует понять причины, которые привели к появлению Hibernate proxy. В прошлой статье, стратегии извлечения данных, были описаны 2 стратегии FetchType.EAGER
и FetchType.LAZY
. В случае FetchType.EAGER
, Hibernate, достает из базы данных связанную сущность вместе с основным объектом, а в случае FetchType.LAZY
, не достает связанную сущность при запросе в базу. На первый взгляд все достаточно просто, но сразу возникает вопрос: если мы не достаем из базы связанные сущности, чем будут инициализированы такие поля?
Когда создается новый объект, мы либо инициализируем все его поля конкретными значениями, либо не инициализированные поля будут инициализированы значениями по умолчанию. Для ссылочных типов, которыми являются все связанные сущности, это null. Таким образом, стратегия извлечения FetchType.LAZY
могла бы стать проблемой, поскольку если из базы данных не будут получены реальные связанные сущности, в такие ссылки должен быть записан null.
Правильно и безопасно реализованный объект должен, либо хранить актуальную информацию, либо бросать ошибку, если по каким-то причинам не может ее предоставить. Объект, хранящий неактуальную информацию в лучшем случае, приведет к ошибке, а в худшем к неправильной работе программы. Hibernate proxy решают именно эту проблему.
Приведем пример, допустим мы создали приложение, в котором модель данных состоит из нескольких объектов Книга (Book), Автор (Author), Рецензии (Reviews).
Теперь представим, что мы создали в репозитории метод для получения книги из базы по id:
public interface BookRepository extends CrudRepository<Book, Long> { @Query("select b from Book b where b.id = ?1") Book getBookById(Long id); }
С помощью созданного метода мы достаем из БД информацию по книге и создаем соответствующий объект book:
... Book book = repository.getBookById(1); ...
Этот метод достает из базы только книгу, без связанных сущностей. То есть, если мы попытаемся узнать имя автора, (book.getAuthor().getName()) или дату первой рецензии (book.getReviews().get(0).getDate()), мы попытаемся прочитать данные, которые не доставали из базы. По понятным причинам мы не можем ни сделать заглушки для этих полей, ни сделать их null, поскольку, такие поля должны хранить актуальное значение. Hibernate proxy нужны именно для того, чтоб гарантировать актуальность значений связанных сущностей. Hibernate proxy, позволяют взаимодействовать с теми полями объекта, которые не доставались из базы, но являются его частью. У созданного объекта book, поля Author author;
и List<Reviews> reviews;
, будут являться прокси классами.
Hibernate прокси, что это?
Hibernate proxy, это прокси класс, наследник связанной сущности. При получении из базы данных объекта, без связанных сущностей, такие поля будут инициализированы не связанными классами, а их наследниками. Такие классы-наследники называются как правило, также как и родительский класс, но с суффиксом Proxy или HibernateProxy.
Это можно увидеть если запустить среду разработки в debag режиме.

Hibernate прокси, как это работает?
Итак мы выяснили, что Hibernate, после запроса в БД, создает объекты из извлеченных данных, при этом объекты, по которым данные не извлекались, заменяются прокси. Если пользователь не будет обращаться к полю инициализированному прокси классом, такой класс будет служить своеобразной заглушкой для соответствующего поля, если же пользователь обратиться к такому полю, проски класс должен обеспечить получение нужных данных или выбросить ошибку, если не сможет гарантировать их актуальность.
На самом деле, Hibernate proxy, реализованы достаточно просто, в таком классе-наследнике, все get и set методы переопределены. В геттеры и сеттеры добавлена проверка на то, что данные были инициализированы ранее. Если данные не были инициализированы, происходит запрос в базу. Если сессия не устарела, данные получаются из базы и записываются в соответствующие поля класса, если сессия устарела, Hibernate не может гарантировать актуальность данных, поэтому выбрасывает ошибку LazyInitializationException.
Прокси генерируются в runtime. Для генерации прокси, с версии 5.3 Hibernate использует Byte Buddy, в более ранних версиях использовались Javassist или CGLIB.
Что нужно учитывать при работе с Hibernate proxy?
Hibernate proxy именно то место, где возникает большая часть N+1 запросов. В тот момент, когда происходит обращение к не инициализированным полям прокси объекта, возникает дополнительный запрос в базу данных. Это и есть +1 дополнительно к N.
Не следует так же забывать, что хотя обращение к прокси классу и происходит через ссылку на родительский класс, созданный класс все же является дочерним.
ссылка на оригинал статьи https://habr.com/ru/articles/895118/
Добавить комментарий