Автор оригинала: Vlad Mihalcea.
Вступление
В этой статье я собираюсь объяснить, как работает механизм JPA и Hibernate первого уровня и как он может повысить производительность вашего уровня доступа к данным.
В терминологии JPA кэш первого уровня называется контекстом сохранения и представлен интерфейсом EntityManager
. В режиме гибернации кэш первого уровня представлен интерфейсом Session
, который расширяет интерфейс JPA EntityManager
.
Состояния сущности JPA и связанные с ними методы перехода в состояние
Объект JPA может находиться в одном из следующих состояний:
- Новое (Переходное)
- Управляемый (Связанный)
- Отстраненный (Диссоциированный)
- Удалено (Удалено)
Чтобы изменить состояние сущности, вы можете использовать методы persist
, merge
или remove
JPA EntityManager
, как показано на следующей диаграмме:
При вызове метода persist
состояние сущности изменяется с нового на Управляемое.
И при вызове метода find
также осуществляется управление состоянием объекта.
После закрытия EntityManager
или вызова метода выселить
состояние сущности становится Отделенным
.
Когда сущность передается в удалить
метод JPA EntityManager
, состояние сущности становится Удалено
.
Реализация кэша первого уровня Hibernate
Внутри Hibernate хранит объекты на следующей карте:
MapentitiesByUniqueKey = new HashMap<>(INIT_COLL_SIZE);
И, EntityUniqueKey
определяется следующим образом:
public class EntityUniqueKey implements Serializable { private final String entityName; private final String uniqueKeyName; private final Object key; private final Type keyType; ... @Override public boolean equals(Object other) { EntityUniqueKey that = (EntityUniqueKey) other; return that != null && that.entityName.equals(entityName) && that.uniqueKeyName.equals(uniqueKeyName) && keyType.isEqual(that.key, key); } ... }
Когда состояние сущности становится Управляемым
, это означает, что оно хранится в этом entitiesByUniqueKey
Java Map
.
Итак, в JPA и Hibernate кэш первого уровня представляет собой Java Map
, в котором ключ Map
представлен объектом, который инкапсулирует имя сущности и ее идентификатор, а значение Map
является самим объектом сущности.
Таким образом, в JPA EntityManager
или в режиме гибернации Сессии
может храниться только одна и только одна сущность , использующая один и тот же идентификатор и тип класса сущности.
Причина, по которой мы можем иметь не более одного представления сущности, хранящегося в кэше первого уровня, заключается в том, что в противном случае мы можем получить разные представления одной и той же строки базы данных, не зная, какая из них является правильной версией, которую следует синхронизировать с соответствующей записью базы данных.
Транзакционная запись за кэшем
Чтобы понять преимущества использования кэша первого уровня, важно понять, как работает стратегия кэширования с последующей записью транзакций.
Как уже объяснялось, методы persist
, merge
и remove
JPA EntityManager
изменяют состояние данного объекта. Однако состояние сущности не синхронизируется каждый раз при вызове метода EntityManager
. На самом деле изменения состояния синхронизируются только при выполнении метода flush
|/EntityManager .
Эта стратегия синхронизации кэша называется отложенной записью и выглядит следующим образом:
Преимущество использования стратегии обратной записи заключается в том, что мы можем паковать несколько объектов при очистке кэша первого уровня.
Стратегия скрытой записи на самом деле очень распространена. Процессор также имеет кэш первого, второго и третьего уровней. И при изменении реестра его состояние не синхронизируется с основной памятью, если только не выполняется очистка.
Кроме того, как объясняется в этой статье , система реляционной базы данных сопоставляет страницы операционной системы со страницами буферного пула в памяти, и по соображениям производительности Буферный пул периодически синхронизируется во время контрольной точки, а не при каждой фиксации транзакции.
Повторяемые считывания на уровне приложения
Когда вы извлекаете объект JPA, либо напрямую:
Post post = entityManager.find(Post.class, 1L);
Или с помощью запроса:
Post post = entityManager.createQuery(""" select p from Post p where p.id = :id """, Post.class) .setParameter("id", 1L) .getSingleResult();
Будет запущено событие Hibernate Load сущности
, которое обрабатывается DefaultLoadEventListener
, которое загрузит сущность следующим образом:
Во-первых, Hibernate проверяет, сохранена ли сущность уже в кэше первого уровня, и если это так, возвращается ссылка на управляемую в данный момент сущность.
Если объект JPA не найден в кэше первого уровня, Hibernate проверит кэш второго уровня, включен ли этот кэш.
Если сущность не найдена в кэше первого или второго уровня, то Hibernate загрузит ее из базы данных с помощью SQL-запроса.
Кэш первого уровня обеспечивает повторяемое чтение на уровне приложения для сущностей, поскольку независимо от того, сколько раз сущность загружается из контекста сохранения, вызывающему объекту будет возвращена одна и та же ссылка на управляемую сущность.
Когда сущность загружается из базы данных, Hibernate принимает набор результатов JDBC и преобразует его в объект Java
[] , известный как состояние загрузки сущности. Загруженное состояние хранится в кэше первого уровня вместе с управляемой сущностью, как показано на следующей диаграмме:
Как вы можете видеть из приведенной выше диаграммы, в кэше второго уровня хранится загруженное состояние, поэтому при загрузке сущности, которая ранее хранилась в кэше второго уровня, мы можем получить загруженное состояние без необходимости выполнения соответствующего SQL – запроса.
По этой причине влияние загрузки сущности на память больше, чем сам объект сущности Java, поскольку загруженное состояние также необходимо сохранить. При очистке контекста сохранения JPA загруженное состояние будет использоваться механизмом проверки на загрязненность, чтобы определить, изменилась ли сущность с момента ее первой загрузки. Если сущность изменилась, будет сгенерировано ОБНОВЛЕНИЕ SQL.
Таким образом, если вы не планируете изменять сущность, то более эффективно загружать ее в режиме только для чтения, так как загруженное состояние будет удалено после создания экземпляра объекта сущности.
Вывод
Кэш первого уровня является обязательной конструкцией в JPA и Hibernate. Поскольку кэш первого уровня привязан к текущему исполняемому потоку, он не может быть совместно использован несколькими пользователями. По этой причине JPA и спящий режим кэша первого уровня не являются потокобезопасными.
Помимо обеспечения повторяемых операций чтения на уровне приложения, кэш первого уровня может паковать несколько операторов SQL за один раз, что увеличивает время отклика на транзакции чтения-записи.
Однако, хотя это предотвращает получение одной и той же сущности несколькими вызовами find
из базы данных, оно не может запретить JPQL или SQL загружать последний снимок сущности из базы данных только для того, чтобы удалить его при сборке набора результатов запроса.