Hibernate¶
ORM (Object-Relational Mapping) - преобразование объектно-ориентированной модели в реляционную и обратно.
Hibernate - Это реализация ORM и самый популярный провайдер JPA. Ключевая функция: взять значения полей
Java-объекта и сохранить их в таблицу БД, а потом собрать объект обратно. Как поля связаны со столбцами, Hibernate
узнаёт из маппинга: аннотаций или XML.
- у Hibernate два API: собственный (native API, Session) и стандартный JPA (EntityManager)
- настраивается через hibernate.cfg.xml (native) или persistence.xml (JPA)
- Hibernate 6 - реализация Jakarta Persistence 3.x, пакеты jakarta.persistence.*
Архитектура Hibernate¶
Configuration (hibernate.cfg.xml)
└─► SessionFactory - одна на приложение
└─► Session - одна на единицу работы
├─► Transaction
├─► Query (HQL, Criteria, native SQL)
└─► persistent-объекты (@Entity)
SessionFactory - Это фабрика Session. Тяжёлый объект, создаётся один раз за жизнь приложения.
- строится из конфигурации: hibernate.cfg.xml, Java-конфигурации или аннотаций
- держит метаданные маппинга, пул соединений, кэш второго уровня
- потокобезопасна, общая для всех потоков
- закрывают при остановке приложения, чтобы освободить ресурсы
- в JPA это EntityManagerFactory, и с Hibernate 5.2 SessionFactory его расширяет
Session - Это основной интерфейс работы с Hibernate: единица работы с БД. Через неё выполняют CRUD (Create,
Read, Update, Delete) и запросы.
- лёгкая и короткоживущая: открыл - поработал - закрыл
- не потокобезопасна: своя сессия на каждый поток или запрос
- внутри держит persistence context - кэш первого уровня и отслеживание изменений (см.
Persistence Context)
- JDBC-соединение берёт из пула только тогда, когда нужен запрос к БД
- в JPA это EntityManager, и с Hibernate 5.2 Session его расширяет
Note
В исходной версии было «Session - одно соединение с базой данных». Session не равна соединению: она берёт соединение из пула по необходимости и отдаёт его обратно, а сама живёт как единица работы.
Transaction - интерфейс управления транзакцией. Изменения в БД оборачивают в транзакцию, чтобы сохранить целостность данных.
Query - интерфейс запросов к БД: HQL (см. HQL), Criteria API и native SQL.
Persistent class (сохраняемый класс) - класс @Entity, который маппится на таблицу. Каждый экземпляр -
одна строка. Hibernate удобнее всего работать с POJO (Plain Old Java Object): обычный класс с полями, геттерами,
сеттерами и конструктором без аргументов. Требования к классу сущности - в
JPA Аннотации классов.
Работа с сессией¶
Сессию открывают перед работой с БД и закрывают после, чтобы освободить ресурсы.
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory(); // один раз на приложение
Transaction tx = null;
try (Session session = sessionFactory.openSession()) {
tx = session.beginTransaction();
MyEntity entity = new MyEntity("Example Name");
session.persist(entity); // INSERT
MyEntity found = session.get(MyEntity.class, entity.getId()); // из кэша первого уровня, без SELECT
found.setName("Updated Name"); // UPDATE сам при коммите: dirty checking
session.remove(found); // DELETE
tx.commit();
} catch (RuntimeException e) {
if (tx != null) {
tx.rollback(); // откат при ошибке
}
throw e;
}
// sessionFactory.close() - при остановке приложения, а не после каждой операции
Note
В исходных примерах были session.save(), update() и delete(). В Hibernate 6 они устарели, вместо них
persist(), merge() и remove() - те же методы, что в JPA. update() для managed-объекта и вовсе не нужен:
изменения полей Hibernate запишет сам при коммите. И sessionFactory.close() в finally после одной операции -
ошибка: фабрику закрывают один раз при остановке приложения.
Основные методы Session:
| Метод | Что делает | Статус в Hibernate 6 |
|---|---|---|
Transaction beginTransaction() |
Начинает транзакцию и возвращает объект Transaction |
|
Transaction getTransaction() |
Транзакция, связанная с сессией | |
void cancelQuery() |
Отменяет выполнение текущего запроса | |
void clear() |
Полностью очищает сессию: все объекты становятся detached | |
void close() |
Заканчивает сессию, освобождает JDBC-соединение и выполняет очистку | |
boolean isOpen() |
Открыта ли сессия | |
boolean isConnected() |
Подключена ли сессия сейчас | |
boolean isDirty() |
Есть ли в сессии изменения, которые ещё не синхронизированы с БД | |
Object get(Class or String entityName, Object id) |
Сохранённый экземпляр по типу или имени сущности и id. Нет такого - null |
|
Object getIdentifier(Object object) |
Идентификатор сущности, связанной с этой сессией | |
void refresh(Object object) |
Перечитывает состояние экземпляра из БД | |
SessionFactory getSessionFactory() |
Фабрика, которая создала эту сессию | |
Serializable save(Object object) |
Сохраняет экземпляр, предварительно назначив сгенерированный id | Устарел, вместо него persist() |
void update(Object object), update(String entityName, Object object) |
Обновляет экземпляр по его id | Устарел, вместо него merge() |
void saveOrUpdate(Object object) |
Сохраняет или обновляет экземпляр | Устарел, вместо него persist() или merge() |
void delete(Object object), delete(String entityName, Object object) |
Удаляет сохранённый экземпляр из БД | Устарел, вместо него remove() |
Query createQuery(String hql) |
Запрос на HQL | Используют типизированный createQuery(hql, Type.class) |
SQLQuery createSQLQuery(String sql) |
Запрос на SQL | Удалён, вместо него createNativeQuery() |
Criteria createCriteria(Class persistentClass), createCriteria(String entityName) |
Legacy Criteria для класса или имени сущности | Удалён, вместо него JPA Criteria API (см. Criteria API) |
Query createFilter(Object collection, String query) |
Запрос по коллекции с фильтром-строкой | Удалён |
Note
В исходной версии было «в интерфейсе Session определены 23 метода». Это выборка из старого туториала по
Hibernate 3: методов в Session намного больше, а часть перечисленных уже удалена. Там же у get() был тип
результата Session, а у close() - Connection: сейчас это Object и void.
Конфигурация¶
hibernate.cfg.xml - Это конфигурация native Hibernate: параметры подключения к БД, диалект SQL, генерация схемы
и список сущностей. Нужна, если работаешь напрямую с Hibernate, без JPA API. new Configuration().configure()
читает её из корня classpath. Вместо XML можно настроить Hibernate Java-кодом.
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"https://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- JDBC -->
<property name="hibernate.connection.driver_class">org.postgresql.Driver</property>
<property name="hibernate.connection.url">jdbc:postgresql://localhost:5432/mydb</property>
<property name="hibernate.connection.username">myuser</property>
<property name="hibernate.connection.password">mypassword</property>
<!-- в Hibernate 6 диалект можно не указывать: он определяется по JDBC-соединению -->
<property name="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</property>
<!-- генерация схемы: validate | update | create | create-drop -->
<property name="hibernate.hbm2ddl.auto">update</property>
<!-- логирование SQL -->
<property name="hibernate.show_sql">true</property>
<property name="hibernate.format_sql">true</property>
<!-- кэш второго уровня -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.jcache.JCacheRegionFactory</property>
<property name="hibernate.javax.cache.uri">ehcache.xml</property>
<!-- сущности: классы с аннотациями или XML-файлы маппинга -->
<mapping class="com.example.domain.User"/>
<mapping class="com.example.domain.Order"/>
<mapping resource="Developer.hbm.xml"/>
</session-factory>
</hibernate-configuration>
persistence.xml для JPA-варианта, вместе с настройкой кэша второго уровня, - в
Конфигурирование.
SessionFactory в CDI. С native API SessionFactory создают явно и сами управляют её жизненным циклом:
- инициализация - new Configuration().configure() читает hibernate.cfg.xml и строит SessionFactory
- внедрение - @Produces отдаёт SessionFactory другим бинам через @Inject
- закрытие - SessionFactory держит ресурсы (например, пул соединений), поэтому её закрывают вручную в
@PreDestroy
@ApplicationScoped
public class HibernateUtil {
private SessionFactory sessionFactory;
@PostConstruct
void init() {
sessionFactory = new Configuration().configure().buildSessionFactory(); // читает hibernate.cfg.xml
}
@Produces
public SessionFactory sessionFactory() {
return sessionFactory; // одна фабрика на всё приложение
}
@PreDestroy
void close() {
sessionFactory.close(); // освободить пул соединений
}
}
С JPA в контейнере всё это не нужно: контейнер сам создаёт EntityManagerFactory, а EntityManager внедряется
через @PersistenceContext (см. EntityManager).
Hibernate в Spring без JPA¶
Старый стиль: Spring создаёт SessionFactory и управляет транзакциями, DAO работает с native Session. Слои:
- конфигурация - SessionFactory в отдельном классе или файле (hibernate.cfg.xml, Java-конфигурация, бин Spring)
- DAO - взаимодействие с БД через Session
- сервис - бизнес-логика поверх DAO, отдельно от доступа к данным
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="your_username"/>
<property name="password" value="your_password"/>
</bean>
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="packagesToScan" value="com.example.model"/> <!-- пакет с сущностями -->
<property name="hibernateProperties">
<props>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
@Repository
public class MyEntityDao {
@Autowired
private SessionFactory sessionFactory;
public void save(MyEntity entity) {
sessionFactory.getCurrentSession().persist(entity); // сессия текущей транзакции Spring
}
public MyEntity findById(Long id) {
return sessionFactory.getCurrentSession().get(MyEntity.class, id);
}
public void delete(MyEntity entity) {
sessionFactory.getCurrentSession().remove(entity);
}
}
@Service
public class MyEntityService {
@Autowired
private MyEntityDao myEntityDao;
@Transactional // граница транзакции - на сервисе
public void createEntity(String name) {
myEntityDao.save(new MyEntity(name));
}
@Transactional(readOnly = true)
public MyEntity getEntity(Long id) {
return myEntityDao.findById(id);
}
}
Note
В исходной версии @Transactional стоял и на DAO, и на сервисе, а в XML не было пространства имён tx, без
которого <tx:annotation-driven> не разберётся. Транзакцию держат на сервисе (см.
Где ставить @Transactional). Сейчас в Spring вместо native Session обычно берут Spring Data
JPA и EntityManager (см. Spring Data).
Итого: в enterprise-приложениях сессиями и SessionFactory управляет фреймворк, бизнес-логика отделена от
доступа к данным, транзакции задаются @Transactional.
Маппинг¶
Как связать Java-класс с таблицей БД - два способа:
1) XML - файл *.hbm.xml на каждый класс. В Hibernate 6 этот формат объявлен устаревшим.
2) Аннотации - @Entity, @Table, @Id, @GeneratedValue, @Column прямо в классе. Основной способ сейчас.
См. JPA Аннотации классов и JPA Аннотации полей.
Note
В исходной версии @Entity описан как «entity bean». Entity bean - компонент старого EJB 2, а @Entity - обычный
POJO-класс JPA, которым управляет EntityManager, а не контейнер (см. Бины в Jakarta EE).
XML-маппинг:
<!-- Developer.hbm.xml -->
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="net.proselyte.hibernate.example.model.Developer" table="HIBERNATE_DEVELOPERS">
<meta attribute="class-description">
This class contains developer's details.
</meta>
<id name="id" type="int" column="ID">
<generator class="native"/>
</id>
<property name="firstName" column="FIRST_NAME" type="string"/>
<property name="lastName" column="LAST_NAME" type="string"/>
<property name="specialty" column="SPECIALTY" type="string"/>
<property name="experience" column="EXPERIENCE" type="int"/>
</class>
</hibernate-mapping>
public class Developer { // POJO без аннотаций: маппинг в Developer.hbm.xml
private int id;
private String firstName;
private String lastName;
private String specialty;
private int experience;
public Developer() {} // обязательный конструктор без аргументов
// конструктор с полями, геттеры, сеттеры, toString
}
Файл маппинга подключают в hibernate.cfg.xml: <mapping resource="Developer.hbm.xml"/> (см.
Конфигурация).
Виды маппинга¶
Связи в ORM делятся на три группы.
Связывание коллекций - поле класса хранит коллекцию значений. Его можно связать через любой интерфейс
коллекций Java: List, Collection, Set, SortedSet, Map, SortedMap.
Ассоциативное связывание - отношения между классами и таблицами БД: - One-to-One - одна сущность связана с одной другой - One-to-Many - одна сущность связана с множеством других - Many-to-One - множество сущностей связано с одной - Many-to-Many - множество сущностей связано с множеством других
См. Связи.
Связывание компонентов - у класса есть поле-ссылка на другой класс, у которого нет своего жизненного цикла: он
полностью зависит от жизненного цикла владельца. Такой класс называют компонентом (component class). В JPA это
@Embeddable + @Embedded: поля компонента хранятся в таблице владельца.
Аннотации Hibernate¶
Аннотации из org.hibernate.annotations.* - расширения сверх JPA. Работают только с Hibernate, при смене провайдера
их придётся убрать.
Загрузка связей¶
Эти аннотации решают проблему N+1 средствами Hibernate. Стандартные решения - JOIN FETCH и EntityGraph - в
N+1 и планы загрузки.
@Fetch - Это способ, которым Hibernate загружает связь. Ставится вместе с @OneToMany, @ManyToMany,
@OneToOne, @ManyToOne. Для to-one связей @Fetch(FetchMode.JOIN) обычно не нужен.
- FetchMode.JOIN - JOIN в SQL при загрузке. Связь фактически становится EAGER
- FetchMode.SELECT - отдельный SELECT на связь. С LAZY - при первом обращении, отсюда N+1
- FetchMode.SUBSELECT - для коллекций: при первом обращении Hibernate загружает коллекции всех родителей из
исходного запроса одним SELECT, подставив исходный запрос подзапросом в WHERE order_id IN (SELECT ...)
@OneToMany(mappedBy = "order")
@Fetch(FetchMode.SUBSELECT)
private List<OrderItem> items;
// загрузили список заказов - потом все order_items придут одним запросом
@BatchSize - Это загрузка ленивых связей пачками. Hibernate не делает JOIN, а грузит лениво, но объединяет
обращения в пачки WHERE id IN (...) указанного размера. Компромисс между JOIN и обычной ленивой загрузкой.
- size - сколько записей грузить одним запросом
@OneToMany(mappedBy = "order")
@BatchSize(size = 20)
private List<OrderItem> items;
// 50 заказов, getItems() у каждого - примерно 3 запроса вместо 50
@Entity
@BatchSize(size = 50)
public class Product { ... }
// ленивый прокси Product инициализируется - Hibernate сразу загрузит до 50 Product по id IN (...)
Глобально для всех ленивых связей - свойство hibernate.default_batch_fetch_size.
@LazyToOne - Это управление ленивостью @OneToOne и @ManyToOne.
- LazyToOneOption.PROXY - вместо объекта прокси, загрузка при первом обращении. Это поведение по умолчанию для
LAZY
- LazyToOneOption.NO_PROXY - без прокси: поле остаётся пустым и загружается при обращении к нему. Нужен bytecode
enhancement. Так делают ленивой обратную сторону @OneToOne, где прокси не работает
- LazyToOneOption.FALSE - эквивалент EAGER
@OneToOne(fetch = FetchType.LAZY)
@LazyToOne(LazyToOneOption.NO_PROXY) // работает только с bytecode enhancement
@JoinColumn(name = "profile_id")
private UserProfile profile;
Note
В исходной версии: @LazyToOne делает связь ленивой «без bytecode enhancement», NO_PROXY создаёт прокси, а
PROXY - «аналогично NO_PROXY». На деле наоборот: PROXY - прокси, а NO_PROXY без enhancement не работает. С
Hibernate 6.2 аннотация устарела: при включённом enhancement ленивость to-one работает без неё.
@FetchProfile - Это заранее описанный план загрузки: какие связи грузить через JOIN. Включается в коде для
конкретной сессии.
- name - имя профиля
- fetchOverrides - массив @FetchProfile.FetchOverride: сущность, связь, режим (FetchMode.JOIN)
@FetchProfile(
name = "order-with-items",
fetchOverrides = @FetchProfile.FetchOverride(entity = Order.class, association = "items", mode = FetchMode.JOIN)
)
@Entity
public class Order { ... }
session.enableFetchProfile("order-with-items");
Order o = session.get(Order.class, id); // Order загрузится вместе с order_items через JOIN
Аннотации сущности¶
| Аннотация | Что делает |
|---|---|
@DynamicInsert |
INSERT только по непустым полям, а не по всем столбцам |
@DynamicUpdate |
UPDATE только изменившихся полей, а не всего объекта |
@SelectBeforeUpdate |
Перед UPDATE делает SELECT и проверяет, нужно ли вообще обновлять строку |
@OptimisticLocking(type = ...) |
Оптимистичная блокировка: VERSION - по @Version, DIRTY - по изменённым полям, ALL - по всем полям, NONE - выключена |
@Immutable |
Сущность только для чтения: изменения не пишутся в БД. Оптимизация для read-only данных |
@Proxy(lazy = false) |
Отключает ленивые прокси для сущности |
@Polymorphism(type = PolymorphismType.EXPLICIT) |
Включать ли класс в полиморфные запросы по родителю |
@Subselect("SELECT ... FROM ...") |
Маппинг сущности на SQL-запрос вместо таблицы, как на представление |
@Synchronize({"table1", "table2"}) |
Таблицы, от которых зависит @Subselect: перед запросом Hibernate сбросит в них изменения и учтёт их для кэша |
@Where(clause = "active = true") |
Условие, которое добавляется ко всем запросам сущности. С Hibernate 6.3 - @SQLRestriction |
@FilterDef, @Filter |
Динамический фильтр: объявляется на сущности, включается в сессии с параметрами |
@Formula |
Вычисляемое поле, см. ниже |
@Entity
@FilterDef(name = "activeFilter", parameters = @ParamDef(name = "active", type = Boolean.class))
@Filter(name = "activeFilter", condition = "active = :active")
public class User { ... }
session.enableFilter("activeFilter").setParameter("active", true); // дальше все запросы User - только active
@Formula¶
@Formula - Это вычисляемое поле: SQL-выражение, которое Hibernate вставляет в SELECT сущности. В таблице такого
столбца нет.
- value - SQL-выражение, которое возвращает одно значение
@Entity
public class Order {
@Id
private Long id;
@Formula("(SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = id)") // количество товаров
private int itemCount;
}
// при выборке Order Hibernate подставит подзапрос и вернёт itemCount вместе с остальными колонками
Кэширование¶
Коротко: L1 - внутри одного EntityManager, L2 - общий для всех EntityManager, query cache - кэш
результатов запросов поверх L2. L2 помогает на повторных чтениях, а не на вставках.
| Уровень | Где живёт | Включён по умолчанию | Что хранит |
|---|---|---|---|
| L0 | Драйвер JDBC, пул, провайдер | Зависит от компонента | Подготовленные выражения, планы запросов |
| L1 | Session / EntityManager |
Да, всегда | Объекты этой сессии |
| L2 | SessionFactory / EntityManagerFactory + провайдер |
Нет | Сущности по id, коллекции, natural id |
| Query cache | SessionFactory + провайдер |
Нет | id результатов запросов + метки времени |
Уровни кэша¶
L0 (опционально) - внутренние кэши драйвера JDBC и пула соединений, кэш планов запросов. Напрямую их обычно не настраивают.
L1 (First-level cache, Session cache) - Это кэш внутри одного EntityManager или Session.
- загруженный объект кладётся в кэш, и повторный запрос того же объекта по id в той же сессии вернёт его из кэша:
второй раз SQL не будет
- живёт, пока жива сессия: обычно один запрос или одна транзакция
- включён всегда, отключить нельзя. Это и есть persistence context (см.
Persistence Context)
L2 (Second-level cache) - Это кэш на уровне SessionFactory (EntityManagerFactory), общий для всех сессий
приложения, между транзакциями и запросами.
- уменьшает число обращений к БД: часто читаемые данные лежат в памяти
- по умолчанию выключен. Включается настройкой, @Cacheable на сущности и стратегией @Cache (см. ниже)
- можно включать и выключать, выбирать провайдер: Infinispan, Ehcache и другие через JCache
- кэширует сущности, коллекции и natural id. Запросы - отдельно, в query cache
- работает и с native API, и с JPA (см. Подключение и persistence.xml)
Query cache - Это кэш результатов конкретных запросов. Его часто называют «рядом с L2».
- хранит не сами объекты, а список их id и метки времени. Объекты по этим id Hibernate берёт из L2 или из БД, поэтому
без L2 кэш запросов почти бесполезен
- нужна та же настроенная фабрика регионов, что и для L2
- включается hibernate.cache.use_query_cache и отдельно для каждого запроса
- сбрасывается при любом изменении таблиц, которые участвуют в запросе: за этим следит регион timestamps
Стратегии @Cache¶
@Cache (org.hibernate.annotations.Cache) - Это настройка кэша второго уровня для конкретной сущности или
коллекции. JPA-аннотация @Cacheable только разрешает кэшировать сущность во втором уровне, а @Cache говорит
Hibernate, как именно.
@Entity
@Cacheable // jakarta.persistence.Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product { ... }
| Стратегия | Когда |
|---|---|
READ_ONLY |
Данные не меняются: справочники. Самая быстрая. Попытка обновить такую сущность - ошибка |
NONSTRICT_READ_WRITE |
Меняются редко. Быстрее READ_WRITE, но может ненадолго отдать устаревшие данные |
READ_WRITE |
Меняются, нужна согласованность. Мягкие блокировки (soft locks) на время обновления: если один поток обновил, другие не получат старое |
TRANSACTIONAL |
Кэш участвует в JTA-транзакции, нужен транзакционный провайдер |
READ_WRITE - правильный дефолт для CRUD: почти все сущности обновляются. READ_ONLY - только если данные
действительно неизменяемы.
Когда L2 срабатывает¶
hit - сущность взяли из кэша, в БД не ходили. miss - искали в кэше, не нашли, пришлось идти в БД. put - положили в кэш.
L2 - кэш по первичному ключу. Он виден на повторном чтении одной и той же сущности по id:
1) em.find(Vehicle.class, id) в первый раз - кэша нет: L2 miss, Hibernate читает из БД и кладёт в кэш - L2 put.
2) em.find(Vehicle.class, id) второй раз, в другой транзакции, когда L1 уже пуст, - L2 hit, в БД не ходит.
Так же по id работают getReference() и ленивая загрузка to-one связей.
Запросы мимо L2. JPQL, Criteria и выборка вроде select v from Vehicle v where v.id in :ids всегда идут в БД
и получают результат пачкой. Hibernate не ищет каждую сущность в L2 по id, поэтому hits и misses не растут. То же с
EntityGraph: связи догружаются SQL-запросом, а не берутся целиком из L2. Это не ошибка, а другой сценарий загрузки.
Note
В исходной заметке было, что при загрузке запросами нормально и puts = 0. С CacheMode.NORMAL (по умолчанию)
Hibernate кладёт в L2 и сущности, загруженные запросом, поэтому puts обычно растут. Если и puts = 0 - проверь, что
на сущности есть @Cacheable, а статистика включена до запроса.
Note
Лаба. L2 включён и настроен: Infinispan + @Cacheable + READ_WRITE. В ручке /query он почти не
срабатывает: там не find() по id, а выборка через IN и EntityGraph. Для демонстрации на защите - сценарий
«двойное чтение по id»: два одинаковых запроса GET /vehicle/{id} подряд в разных транзакциях, первый - miss + put,
второй - hit.
Регионы и Infinispan¶
Регион - Это именованная область кэша для одного вида данных. Hibernate раскладывает L2 по регионам, провайдер хранит каждый регион как отдельный кэш.
<!-- infinispan-config-local.xml -->
<infinispan xmlns="urn:infinispan:config:15.0">
<cache-container name="hibernate"> <!-- контейнер кэшей для интеграции Hibernate -->
<local-cache name="default"/> <!-- fallback для регионов без своего описания -->
<local-cache name="entity"/> <!-- сущности с @Cacheable / @Cache -->
<local-cache name="query"/> <!-- query cache -->
<local-cache name="timestamps"/> <!-- метки изменений таблиц -->
</cache-container>
</infinispan>
<infinispan>- корневой элемент конфигурации, схема 15.0<cache-container name="hibernate">- контейнер кэшей. Интеграция Hibernate с Infinispan ищет и создаёт регионы именно в контейнере для Hibernatedefault- регион по умолчанию: используется, если Hibernate попросит регион, который не описан явноentity- сущности. Первый запрос - БД и put вentity, второй - hit изentityquery- результаты запросов, если включёнhibernate.cache.use_query_cache. Хранит не сущности, а список id и параметры, а сущности по этим id берутся изentityили из БДtimestamps- ключевой регион для корректности query cache. Hibernate хранит в нём время последнего изменения каждой таблицы. Кто-то вставил или обновил сущность - метка меняется, и результаты query cache по этой таблице становятся устаревшими, Hibernate их не отдаст
local-cache - кэш в памяти одной JVM: без кластера и репликации, по сути Map с внутренними структурами.
Note
Почему local-конфиг для лабы. Кластер и распределённое хранение не нужны. Важно показать, что L2 реально
работает: есть hits и misses, есть регионы query и timestamps, и всё стабильно на одном сервере.
Статистика кэша¶
Hibernate считает hits, misses и puts в Statistics. Чтобы видеть вклад конкретного метода, удобно повесить на него
CDI-перехватчик.
Перехватчик (interceptor) - Это перехватчик вызовов метода: свой код до и после метода, не меняя сам метод (см. Возможности CDI).
1. Переключатель - CDI-бин на всё приложение с флагом «логировать или нет». Стартовое значение - из system
property: JVM с -Dl2cache.stats.enabled=true включит логирование. AtomicBoolean потокобезопасен и позволяет
менять флаг на лету, например через админский endpoint.
@ApplicationScoped
public class L2CacheStatsToggle {
private final AtomicBoolean enabled = new AtomicBoolean(Boolean.getBoolean("l2cache.stats.enabled"));
public boolean isEnabled() { return enabled.get(); }
public void setEnabled(boolean value) { enabled.set(value); }
}
2. Interceptor binding - метка, по которой CDI понимает, куда применять перехватчик. Ставится на класс сервиса (логируются все методы) или на конкретный метод.
@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface L2CacheStats {}
// @L2CacheStats над VehicleService - все методы, над getVehicleById() - только он
3. Доступ к Statistics. JPA даёт EntityManagerFactory. Hibernate - провайдер, поэтому
emf.unwrap(SessionFactory.class) возвращает его SessionFactory, а у неё есть Statistics. Статистику включают,
если она выключена: по умолчанию её нет, потому что счётчики стоят накладных расходов. Глобально - свойство
hibernate.generate_statistics.
@ApplicationScoped
public class HibernateStatsService {
@PersistenceUnit
EntityManagerFactory emf;
public Statistics stats() {
Statistics st = emf.unwrap(SessionFactory.class).getStatistics();
if (!st.isStatisticsEnabled()) {
st.setStatisticsEnabled(true);
}
return st;
}
}
4. Перехватчик - снимает счётчики L2 до и после метода и логирует разницу.
@L2CacheStats // срабатывает только там, где стоит @L2CacheStats
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 50) // включает перехватчик и задаёт порядок среди других
public class L2CacheStatsInterceptor {
private static final Logger log = Logger.getLogger(L2CacheStatsInterceptor.class.getName());
@Inject
L2CacheStatsToggle toggle;
@Inject
HibernateStatsService statsService;
@AroundInvoke
public Object around(InvocationContext ctx) throws Exception {
if (!toggle.isEnabled()) {
return ctx.proceed(); // логирование выключено - просто выполнить метод
}
Statistics st;
try {
st = statsService.stats();
} catch (Exception e) {
// статистика недоступна - не ломать бизнес-метод, только пропустить логирование
log.log(Level.WARNING, "L2 stats unavailable (no Hibernate SessionFactory?)", e);
return ctx.proceed();
}
long hitsBefore = st.getSecondLevelCacheHitCount(); // нашли в L2, без похода в БД
long missBefore = st.getSecondLevelCacheMissCount(); // L2 не помог, пошли в БД
long putsBefore = st.getSecondLevelCachePutCount(); // положили в L2
Object res = ctx.proceed(); // реальный бизнес-метод
long dh = st.getSecondLevelCacheHitCount() - hitsBefore;
long dm = st.getSecondLevelCacheMissCount() - missBefore;
long dp = st.getSecondLevelCachePutCount() - putsBefore;
if (dh != 0 || dm != 0 || dp != 0) { // логировать только изменения, чтобы не спамить
log.info(() -> "[L2] " + ctx.getMethod() + ": hits +" + dh + ", miss +" + dm + ", puts +" + dp);
}
return res; // поведение метода не меняется
}
}
Warning
Statistics - глобальные счётчики на всё приложение. Если параллельно идут другие запросы, в дельту попадёт и их
работа с кэшем. Для точных цифр метод вызывают в тишине или смотрят на статистику по региону
(st.getCacheRegionStatistics("entity")).
HQL¶
HQL (Hibernate Query Language) - Это объектно-ориентированный язык запросов, очень похожий на SQL. - SQL работает с таблицами и столбцами, HQL - с persistent-объектами и их полями - Hibernate транслирует HQL в SQL, понятный конкретной БД - HQL - надмножество JPQL: любой JPQL-запрос - корректный HQL - обычный SQL тоже доступен через native-запросы, но HQL предпочтительнее: он не привязан к диалекту БД
FROM - загрузить объекты в память. Developer здесь - имя класса, а не таблицы.
SELECT - получить отдельные поля.
List<String> lastNames = session.createQuery("SELECT D.lastName FROM Developer D", String.class).list();
AS - алиас. Developer D и Developer AS D - одно и то же.
WHERE - условие. Значения передают параметрами, а не склейкой строк.
session.createQuery("FROM Developer D WHERE D.id = :id", Developer.class)
.setParameter("id", 1)
.list();
ORDER BY - сортировка по полю: по возрастанию ASC или по убыванию DESC.
session.createQuery("FROM Developer D WHERE D.experience > 3 ORDER BY D.experience DESC", Developer.class).list();
GROUP BY - группировка по признаку. Все поля в SELECT, кроме агрегатов, должны быть в GROUP BY.
session.createQuery("SELECT MAX(D.experience), D.lastName, D.specialty FROM Developer D "
+ "GROUP BY D.lastName, D.specialty", Object[].class).list();
UPDATE и DELETE - массовые операции, выполняются через executeUpdate() в транзакции. Они идут прямо в БД
мимо кэша первого уровня.
session.createMutationQuery("UPDATE Developer SET experience = :experience WHERE id = :developerId")
.setParameter("experience", 3)
.setParameter("developerId", 10)
.executeUpdate();
session.createMutationQuery("DELETE FROM Developer WHERE id = :developerId")
.setParameter("developerId", 1)
.executeUpdate();
INSERT - добавить запись. До Hibernate 6 поддерживался только INSERT ... SELECT, с Hibernate 6 есть и
VALUES.
session.createMutationQuery("INSERT INTO Developer (firstName, lastName, specialty, experience) "
+ "VALUES ('Ivan', 'Petrov', 'Java', 3)")
.executeUpdate();
Note
Примеры в исходной версии не работали: у INSERT не было ни VALUES, ни SELECT, у UPDATE строка была без кавычек
и с опечаткой в имени параметра (expericence), у UPDATE и DELETE не было executeUpdate(). В GROUP BY поле
specialty было в SELECT, но не в группировке - PostgreSQL такой запрос отвергнет.
Агрегатные функции - те же, что в SQL:
| Функция | Что возвращает |
|---|---|
min(свойство) |
Минимальное значение свойства |
max(свойство) |
Максимальное значение свойства |
sum(свойство) |
Сумма всех значений свойства |
avg(свойство) |
Среднее арифметическое значений свойства |
count(свойство) |
Количество строк, где свойство не NULL. count(*) или count(D) - количество строк |
Native API или JPA¶
Native API (Hibernate API) - оригинальный API Hibernate через org.hibernate.Session. Даёт больше возможностей,
чем JPA: собственные расширения HQL, @Filter, natural id, multi-tenancy, тонкий контроль над сессией и
транзакциями.
JPA API - стандартная спецификация ORM, её поддерживают разные провайдеры: Hibernate, EclipseLink, OpenJPA.
Работает через EntityManager, EntityTransaction и аннотации @Entity, @Table, @Column. Абстрагирует от
провайдера и упрощает переход на другую реализацию.
// native API
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
Employee emp = new Employee();
emp.setName("John Doe");
emp.setSalary(50000);
session.persist(emp);
tx.commit();
session.close();
// JPA API
EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-persistence-unit");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Employee emp2 = new Employee();
emp2.setName("Jane Doe");
emp2.setSalary(60000);
em.persist(emp2);
em.getTransaction().commit();
em.close();
| Native API | JPA API | |
|---|---|---|
| Спецификация | Проприетарный API Hibernate | Стандарт Jakarta EE |
| Возможности | Больше: расширения Hibernate | Только стандартные |
| Контроль за поведением | Полный, напрямую через Session и SessionFactory |
Через абстрактный EntityManager, фабрикой и менеджером управляет контейнер |
| Переносимость | Только Hibernate | Любой JPA-провайдер |
| Конфигурация | hibernate.cfg.xml |
persistence.xml |
| Создание фабрики | SessionFactory создаёшь и закрываешь сам |
В контейнере EntityManagerFactory создаёт контейнер |
| Внедрение | Своя SessionFactory, например через CDI-продюсер |
@PersistenceContext |
| Сложность настройки | Выше: ресурсами управляешь вручную | Ниже, особенно в контейнере: стандартизированный подход |
| Где удобен | Standalone-приложения или явная настройка | Jakarta EE и Spring |
Note
В исходной таблице было «кэширование второго уровня - встроенная поддержка в Hibernate API, в JPA требуется
настройка». Кэш второго уровня - свойство SessionFactory, он одинаково работает через оба API и в обоих случаях
требует настройки. А Session и EntityManager в Hibernate 6 - один объект: из JPA можно получить native API
через em.unwrap(Session.class).
Что выбрать:
- приложение в контейнере (Jakarta EE или Spring) - JPA с @PersistenceContext: проще и меньше ошибок
- standalone-приложение - тоже JPA, но EntityManagerFactory создаётся вручную через
Persistence.createEntityManagerFactory()
- native API - только если нужны специфичные возможности Hibernate или переносимость на другой ORM точно не
нужна. Можно и комбинировать: основной код на JPA, а там, где нужно больше гибкости, - unwrap(Session.class)