В статье о поддержке пользовательских типов в Hibernate упоминается о поддержке кэширования. В этой статье я постараюсь рассказать о кэшровании подробнее.
Идея кэширования (не только в Hibernate) основывается на мнении, что из всех данных, доступных для обработки, работа ведётся только над некоторым небольшим набором и если ускорить доступ к этому набору, то в среднем программа будет работать быстрее. Говоря конкретно о Hibernate, доступ к базе занимает на порядке больше времени, чем доступ к объекту в памяти JVM. И поэтому, если какое-то время хранить в памяти загруженные из БД объекты, то при их повторном запросе Hibernate сможет вернуть их гораздо быстрее.
Кэш первого уровня
С объектом Session, а точнее с persistence context, в Hibernate всегда связан кэш первого уровня. При помещении объекта в persistence context, то есть при его загрузке из БД или сохранении, объект так же автоматически будет помещён в кэш первого уровня и это невозможно отключить. Соответственно, при запросах того же самого объекта несколько раз в рамках одного persistence context, запрос в БД будет выполнен один раз, а всё остальные загрузки будут выполнены из кэша.
Session session = sessionFactory.openSession(); session.beginTransaction(); // Database will be queried System.out.println(session.get(Person.class, 123456L)); // Cached object will be returned System.out.println(session.get(Person.class, 123456L)); session.getTransaction().commit(); session.close();
В примере выше только первый вызов get() инициирует запрос к базе, второй вызов будет обслужен уже из кэша и обращения к базе не произойдёт.
Интересно поведение кэша первого уровня при использовании ленивой загрузки. При загрузке объекта методом load() или объекта с лениво загружаемыми полями, лениво загружаемые данные в кэш не попадут. При обращении к данным будет выполнен запрос в базу и данные будут загружены и в объект и в кэш. А вот следующая попытка лениво загрузить объект приведёт к тому, что объект сразу вернут из кэша и уже полностью загруженным.
session = sessionFactory.openSession(); session.beginTransaction(); // Database is not queried, only reference is returned Person p1 = session.load(Person.class, 123456L); // Query triggered, object is filled with data System.out.println(p1); // Cached, fully populated object will be returned Person p2=session.load(Person.class, 123456L); System.out.println(p2); session.getTransaction().commit(); session.close();
Кэш второго уровня
Если кэш первого уровня существует только на уровне сессии и persistence context, то кэш второго уровня находится выше — на уровне SessionFactory и, следовательно, один и тот же кэш доступен одновременно в нескольких persistence context. Кэш второго уровня требует некоторой настройки и поэтому не включен по умолчанию. Настройка кэша заключается в конфигурировании реализации кэша и разрешения сущностям быть закэшированными.
Конфигурирование кэша
Hibernate не реализует сам никакого in-memory сache, а использует существующие реализации кэшей. Раньше Hibernate самостоятельно поддерживал интерфейс с этими кэшами, но сейчас существует JCache и корректнее будет использовать этот интерфейс. Реализаций у JCache множество, но я выберу ehcache, как одну из самых распространённых.
В первую очередь надо добавить поддержку JCache и ehcache в зависимости:
<properties> <hibernate.version>5.2.0.Final</hibernate.version> <ehcache.version>3.1.1</ehcache.version> </properties> <dependencies> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>${ehcache.version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-jcache</artifactId> <version>${hibernate.version}</version> </dependency> </dependencies>
Затем настроить hibernate на использование ehcache для кэширования:
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.jcache.JCacheRegionFactory</property> <property name="hibernate.javax.cache.provider">org.ehcache.jsr107.EhcacheCachingProvider</property>
В первой строке мы говорим Hibernate, что хотим использовать JCache интерфейс, а во второй строке выбираем конкретную реализацию JCache: ehcache.
Наконец, включим кэш второго уровня:
<property name="hibernate.cache.use_second_level_cache">true</property>
@Cacheable и @Cache
@Cacheable это аннотация JPA и позволяет объекту быть закэшированным. Hibernate поддерживает эту аннотацию в том же ключе.
@Entity @Cacheable public class Person extends AbstractIdentifiableObject { @Getter @Setter private String firstName; //Other fields }
@Cache это аннотация Hibernate, настраивающая тонкости кэширования объекта в кэше второго уровня Hibernate. Аннотации @Cacheable достаточно, чтобы объект начал кэшироваться с настройками по умолчанию. При этом @Cache использованная без @Cacheable не разрешит кэширование объекта.
@Cache принимает три параметра:
- include, имеющий по умолчанию значение all и означающий кэширование всего объекта. Второе возможное значение, non-lazy, запрещает кэширование лениво загружаемых объектов. Кэш первого уровня не обращает внимания на эту директиву и всегда кэширует лениво загружаемые объекты.
- region позволяет задать имя региона кэша для хранения сущности. Регион можно представить как разные кэши или разные части кэша, имеющие разные настройки на уровне реализации кэша. Например, я мог бы создать в конфигурации ehcache два региона, один с краткосрочным хранением объектов, другой с долгосрочным и отправлять часто изменяющиеся объекты в первый регион, а все остальные во второй.
- usage задаёт стратегию одновременного доступа к объектам.
Последний пункт достаточно объёмен, чтобы рассматривать его внутри списка. Проблема заключается в том, что кэш второго уровня доступен из нескольких сессий сразу и несколько потоков программы могут одновременно в разных транзакциях работать с одним и тем же объектом. Следовательно надо как-то обеспечивать их одинаковым представлением этого объекта.
Стратегий одновременного доступа к объектам в кэше в hibernate существует четыре:
- translactional — полноценное разделение транзакций. Каждая сессия и каждая транзакция видят объекты, как если бы только они с ним работали последовательно одна транзакция за другой. Плата за это — блокировки и потеря производительности.
- read-write — полноценный доступ к одной конкретной записи и разделение её состояния между транзакциями. Однако суммарное состояние нескольких объектов в разных транзакциях может отличаться.
- nonstrict-read-write — аналогичен read-write, но изменения объектов могут запаздывать и транзакции могут видеть старые версии объектов. Рекомендуется использовать в случаях, когда одновременное обновление объектов маловероятно и не может привести к проблемам.
- read-only — объекты кэшируются только для чтения и изменение удаляет их из кэша.
Список выше отсортирован по нарастанию производительности, transactional стратегия самая медленная, read-only самая быстрая. Недостатком read-only стратегии является её бесполезность, в случае если объекты постоянно изменяются, так как в этом случае они не будут задерживаться в кэше.
Использование кэша второго уровня требует изменений в конфигурации Hibernate и в коде сущностей, но не требует изменения кода запросов и управления сущностями:
session = sessionFactory.openSession(); session.beginTransaction(); // Database will be queried System.out.println(session.get(Person.class, 3L)); session.getTransaction().commit(); session.close(); session = sessionFactory.openSession(); session.beginTransaction(); // Database will not be queried, 2nd level cache will provide the data System.out.println(session.get(Person.class, 3L)); session.getTransaction().commit(); session.close();
Кэш запросов
Кэши первого и второго уровней работают с объектами загружаемыми по id. Но в дикой природе к базе чаще выполняются запросы с условиями, чем загружаются какие-то заранее известные объекты:
session.createCriteria(Passport.class) .add(Restrictions.eq("series", "AS")) .uniqueResult()
И результат выполнения таких запросов тоже может потребоваться кэшировать. Например если вы делаете поисковый сайт по автозапчастям, то можете кэшировать запросы пользователей, которые, скорее всего, ищут одни запчасти гораздо чаще других. У кэша запросов есть и своя цена — Hibernate будет вынужден отслеживать сущности закешированные с определённым запросом и выкидывать запрос из кэша, если кто-то поменяет значение сущности. То есть для кэша запросов стратегия параллельного доступа всегда read-only.
Чтобы включить кэш запросов надо настроить внешний кэш, так же как и для кэша второго уровня, и разрешить Hibernate кэшировать запросы:
<property name="hibernate.cache.use_query_cache">true</property>
Но даже с этим разрешением Hibernate не будет кэшировать все запросы, а только те, кэширование которых явно запрошено методом setCacheable()
session = sessionFactory.openSession(); session.beginTransaction(); // Database will be queried System.out.println(session.createCriteria(Passport.class) .add(Restrictions.eq("series", "AS")) .setCacheable(true) .uniqueResult()); session.getTransaction().commit(); session.close(); session = sessionFactory.openSession(); session.beginTransaction(); // Database will not be queries, query cache will provide the data System.out.println(session.createCriteria(Passport.class) .add(Restrictions.eq("series", "AS")) .setCacheable(true) .uniqueResult()); session.getTransaction().commit(); session.close();
Кэш запросов, так же как и кэш второго уровня, существует на уровне SessionFactory и доступен во всех persistence context.