Перейти к содержанию

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 ищет и создаёт регионы именно в контейнере для Hibernate
  • default - регион по умолчанию: используется, если Hibernate попросит регион, который не описан явно
  • entity - сущности. Первый запрос - БД и put в entity, второй - hit из entity
  • query - результаты запросов, если включён 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 здесь - имя класса, а не таблицы.

List<Developer> developers = session.createQuery("FROM Developer", Developer.class).list();

SELECT - получить отдельные поля.

List<String> lastNames = session.createQuery("SELECT D.lastName FROM Developer D", String.class).list();

AS - алиас. Developer D и Developer AS D - одно и то же.

session.createQuery("FROM Developer AS D", Developer.class).list();

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)