Кэш первого и второго уровня в Hibernate и READ_ONLY CacheConcurrencyStrategy

В этой статье мы рассмотрим, что такое кэш первого и второго уровня в Hibernate на примере Spring Boot приложения.

Кэш первого уровня – Session (либо EntityManager)

Кэшем первого уровня в Hibernate считается сессия (либо EntityManager – ее аналог в JPA). Включать в настройках этот кэш не нужно, так как по сути это и не кэш, а одно название – так по умолчанию работает Hibernate под капотом.

А именно: Hibernate хранит отслеживаемые сущности в Map, ключами которой являются id сущностей, а значениями – сами объекты-сущности. Если мы извлекаем из базы сущность по id с помощью EntityManager.find(), то сущность помещается в этот Map и хранится в нем до закрытия сессии. И при повторном find() SQL-команда select в базе данных выполнена не будет. Hibernate возьмет эту сущность из Map – карты отслеживаемых сущностей.

Пример

Извлекать из базы сущности мы будем в тесте Spring Boot приложения. На старте приложения (и теста) а базу добавляются некоторые данные. А в настройках включено отображение SQL-команд в консоли.

Тест помечен аннотацией @Transactional, что означает, что он выполняется в рамках транзакции. То есть в начале и в конце теста неявно выполняется begin() и commit() транзакции. Мы эти методы не видим, их вызывает Spring с помощью AOP.

Также транзакция привязана к жизненному циклу сессии Hibernate. То есть помимо начала и подтверждения транзакции, происходит открытие и закрытие сессии. Перед началом транзакции сессия открывается, а после подтверждения закрывается. И Map отслеживаемых сущностей, который мы назвали кэшем первого уровня, тоже живет ровно то время, пока жива сессия (в нашем примере не сессия, а ее аналог EntityManager).

Итак, давайте вызовем в тесте em.find() дважды:

@SpringBootTest
class FirstLevelCacheTest {

    @PersistenceContext
    private EntityManager em;

    @Test
    @Transactional
    public void when2Finds_thenOnefind() {
        City city=em.find(City.class,1l);
        System.out.println("find1");
        Assertions.assertEquals("city1", city.getName());
        City city2=em.find(City.class,1l);
        System.out.println("find2");
        Assertions.assertEquals("city1", city.getName());
    }
...

}

В результате такого теста сгенерируется только один SQL-select:

Hibernate: select city0_.id as id1_0_0_, city0_.code as code2_0_0_, city0_.name as name3_0_0_ from city city0_ where city0_.id=?
find1
find2

Второй раз при вызове find() Hibernate возьмет сущность из кэша первого уровня. Он еще называется Persistence Context. Или хранилище отслеживаемых сущностей.

Обратите внимание что EntityManager так и аннотирован говорящей аннотацией @PersistenceContext.

Кэш второго уровня – общий кэш всех сессий SessionFactory

Если сессия обычно (у нас точно) привязана к транзакции и закрывается каждый раз по ее окончании, то SessionFactory создается один раз на все приложение. Этот кэш и считается кэшем второго уровня, но по умолчанию он не работает – его надо включать.

Кэш второго уровня – это прослойка, общая для всех сессий. То есть одна сессия извлекла сущность, а другая может получить к этой сущности потом доступ.

Очевидно, что с такой прослойкой есть проблема – ее данные могут устареть. В базе данные одни, а в кэше второго уровня – другие. Особенно, если помимо нашего приложения базу обновляет еще какой-то процесс. Но иногда все-таки от кэша второго уровня есть польза. Например, если наша сущность в принципе не редактируема (такой вот задан функционал), а доступна только для чтения. В этом случае почему бы не дать возможность всем сессиям брать готовую сущность, а не извлекать ее из базы каждый раз заново. Все равно сущность не устареет (так как она неизменяемая).

Включение кэша второго уровня

Рассмотрим пример. Только сначала включим кэш второго уровня. Для этого надо добавить в POM-файл какой-либо кэш, например Ehcache:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>5.4.21.Final</version>
</dependency>

И включим кэш в настройках – в application.yml:

spring:
  ...
  jpa:
    ...
    properties:
      hibernate:
        cache:
          use_second_level_cache: false
          region:
            factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory

Теперь можно извлекать сущность в двух разных сессиях, для этого сделаем два @Transactional-теста, для каждого из будет открываться своя сессия и выполняться em.find() сущности City с id=1.

Только сначала пометим сущность City как кэшируемую, поскольку кэш второго уровня включается не для всех сущностей сразу, а только для помеченных аннотацией @org.hibernate.annotations.Cache:

@Data
@NoArgsConstructor
@Entity
@org.hibernate.annotations.Cache(
        usage = CacheConcurrencyStrategy.READ_ONLY
)
public class City {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
    private String name;
}

READ_ONLY CacheConcurrencyStrategy

Обратите внимание, что мы пометили сущность кэшируемой, выбрав стратегию CacheConcurrencyStrategy.READ_ONLY, что и подразумевает, что сущность не редактируется, а доступна только для чтения.

Итак, выполним два теста – в каждом из них мы пытаемся найти в базе City с id=1. Сессия у каждого теста своя, она закрывается по окончании теста. Но при этом SQL будет выполнен один раз для двух тестов, потому что второй раз сущность берется из кэша второго уровня.

@SpringBootTest
class SecondLevelCacheTest {

    @PersistenceContext
    private EntityManager em;


    @Test
    @Transactional
    public void find1() {
       City city=em.find(City.class,1l);
        System.out.println("select1");
        Assertions.assertEquals("city1", city.getName());
    }

    @Test
    @Transactional
    public void find2() {
        City city=em.find(City.class,1l);
        System.out.println("select2");
        Assertions.assertEquals("city1", city.getName());
    }

}

Генерируемый SQL:

Hibernate: select city0_.id as id1_0_0_, city0_.code as code2_0_0_, city0_.name as name3_0_0_ from city city0_ where city0_.id=?
select1
select2

После первого select City помещается в кэш второго уровня. Поэтому для второго теста SQL- команда select не выполняется, что и видно выше.

Если же в application.yml отключить кэш второго уровня (поставить false), то в к консоли мы увидим две SQL-команды select:

Hibernate: select city0_.id as id1_0_0_, city0_.code as code2_0_0_, city0_.name as name3_0_0_ from city city0_ where city0_.id=?
select1
Hibernate: select city0_.id as id1_0_0_, city0_.code as code2_0_0_, city0_.name as name3_0_0_ from city city0_ where city0_.id=?
select2

Что означает, что City не кэшируется.

Итоги

Мы рассмотрели на примерах, что такое кэш первого и второго уровня в Hibernate, и как включить кэш второго уровня.

Стратегию кэширования CacheConcurrencyStrategy.READ_ONLY стоит выбирать для неизменяемых сущностей. С этой стратегией сущности помещаются в кэш второго уровня при извлечении их из базы. А удаляются из кэша при удалении их из базы.

Стоит отметить, что попытка редактировать сущность с CacheConcurrencyStrategy.READ_ONLY вызовет исключение.

Исходный код приложения доступен на GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *