Вступление
Ранее я описал структуру записи кэша второго уровня , которую Hibernate использует для хранения сущностей. Помимо сущностей, Hibernate также может хранить ассоциации сущностей, и эта статья расскажет о внутренней работе кэширования коллекций.
Доменная модель
Для предстоящих тестов мы будем использовать следующую модель сущности:
Репозиторий имеет коллекцию объектов Commit :
1
2
3
4
5
6
|
@org .hibernate.annotations.Cache( usage = CacheConcurrencyStrategy.READ_WRITE ) @OneToMany (mappedBy = "repository" , cascade = CascadeType.ALL, orphanRemoval = true ) private List<Commit> commits = new ArrayList<>(); |
Каждый объект Commit имеет коллекцию встраиваемых элементов Change .
01
02
03
04
05
06
07
08
09
10
|
@ElementCollection @CollectionTable ( name= "commit_change" , joinColumns = @JoinColumn (name= "commit_id" ) ) @org .hibernate.annotations.Cache( usage = CacheConcurrencyStrategy.READ_WRITE ) @OrderColumn (name = "index_id" ) private List<Change> changes = new ArrayList<>(); |
И теперь мы вставим некоторые тестовые данные:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
doInTransaction(session -> { Repository repository = new Repository( "Hibernate-Master-Class" ); session.persist(repository); Commit commit1 = new Commit(); commit1.getChanges().add( new Change( "README.txt" , "0a1,5..." ) ); commit1.getChanges().add( new Change( "web.xml" , "17c17..." ) ); Commit commit2 = new Commit(); commit2.getChanges().add( new Change( "README.txt" , "0b2,5..." ) ); repository.addCommit(commit1); repository.addCommit(commit2); session.persist(commit1); }); |
Прочитанное кеширование
Кэш коллекции использует сквозную стратегию синхронизации :
1
2
3
4
5
6
7
|
doInTransaction(session -> { Repository repository = (Repository) session.get(Repository. class , 1L); for (Commit commit : repository.getCommits()) { assertFalse(commit.getChanges().isEmpty()); } }); |
и коллекции кэшируются при первом обращении:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
select collection0_.id as id1_0_0_, collection0_. name as name2_0_0_ from Repository collection0_ where collection0_.id=1 select commits0_.repository_id as reposito3_0_0_, commits0_.id as id1_1_0_, commits0_.id as id1_1_1_, commits0_.repository_id as reposito3_1_1_, commits0_.review as review2_1_1_ from commit commits0_ where commits0_.r select changes0_.commit_id as commit_i1_1_0_, changes0_.diff as diff2_2_0_, changes0_.path as path3_2_0_, changes0_.index_id as index_id4_0_ from commit_change changes0_ where changes0_.commit_id=1 select changes0_.commit_id as commit_i1_1_0_, changes0_.diff as diff2_2_0_, changes0_.path as path3_2_0_, changes0_.index_id as index_id4_0_ from commit_change changes0_ where changes0_.commit_id=2 |
После кэширования репозитория и связанных с ним коммитов загрузка репозитория и обход коллекций Commit и Change не попадут в базу данных, поскольку все сущности и их ассоциации обслуживаются из кэша второго уровня:
1
2
3
4
5
6
|
LOGGER.info( "Load collections from cache" ); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository. class , 1L); assertEquals( 2 , repository.getCommits().size()); }); |
При выполнении предыдущего контрольного примера оператор SQL SELECT не выполняется:
1
2
|
CollectionCacheTest - Load collections from cache JdbcTransaction - committed JDBC Connection |
Структура записи кэша коллекции
Для коллекций сущностей Hibernate хранит только идентификаторы сущностей, поэтому требуется также кэширование сущностей:
01
02
03
04
05
06
07
08
09
10
|
key = {org.hibernate.cache.spi.CacheKey@3981} key = {java.lang.Long@3597} "1" type = {org.hibernate. type .LongType@3598} entityOrRoleName = {java.lang.String@3599} "com.vladmihalcea.hibernate.masterclass.laboratory.cache.CollectionCacheTest$Repository.commits" tenantId = null hashCode = 31 value = {org.hibernate.cache.ehcache.internal.strategy.AbstractReadWriteEhcacheAccessStrategy$Item@3982} value = {org.hibernate.cache.spi.entry.CollectionCacheEntry@3986} "CollectionCacheEntry[1,2]" version = null timestamp = 5858841154416640 |
CollectionCacheEntry хранит идентификаторы Commit, связанные с данным объектом Repository .
Поскольку типы элементов не имеют идентификаторов, Hibernate сохраняет их обезвоженное состояние. Встраиваемое изменение кэшируется следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
key = {org.hibernate.cache.spi.CacheKey@3970} "com.vladmihalcea.hibernate.masterclass.laboratory.cache.CollectionCacheTest$Commit.changes#1" key = {java.lang.Long@3974} "1" type = {org.hibernate. type .LongType@3975} entityOrRoleName = {java.lang.String@3976} "com.vladmihalcea.hibernate.masterclass.laboratory.cache.CollectionCacheTest$Commit.changes" tenantId = null hashCode = 31 value = {org.hibernate.cache.ehcache.internal.strategy.AbstractReadWriteEhcacheAccessStrategy$Item@3971} value = {org.hibernate.cache.spi.entry.CollectionCacheEntry@3978} state = {java.io.Serializable[2]@3980} 0 = {java.lang.Object[2]@3981} 0 = {java.lang.String@3985} "0a1,5..." 1 = {java.lang.String@3986} "README.txt" 1 = {java.lang.Object[2]@3982} 0 = {java.lang.String@3983} "17c17..." 1 = {java.lang.String@3984} "web.xml" version = null timestamp = 5858843026345984 |
Модель согласованности кэша коллекции
Последовательность является самой большой проблемой при использовании кэширования , поэтому нам нужно понять, как Hibernate Collection Cache обрабатывает изменения состояния сущностей.
CollectionUpdateAction отвечает за все изменения коллекции, и при каждом изменении коллекции соответствующая запись кэша удаляется:
01
02
03
04
05
06
07
08
09
10
|
protected final void evict() throws CacheException { if ( persister.hasCache() ) { final CacheKey ck = session.generateCacheKey( key, persister.getKeyType(), persister.getRole() ); persister.getCacheAccessStrategy().remove( ck ); } } |
Это поведение также задокументировано спецификацией CollectionRegionAccessStrategy :
Для кэшированных данных коллекции все действия по изменению фактически просто делают недействительными записи.
Исходя из текущей стратегии параллелизма, запись в кэше коллекции удаляется:
- до принятия текущей транзакции, для CacheConcurrencyStrategy.NONSTRICT_READ_WRITE
- сразу после фиксации текущей транзакции, для CacheConcurrencyStrategy.READ_WRITE
- точно, когда текущая транзакция зафиксирована, для CacheConcurrencyStrategy.TRANSACTIONAL
Добавление новых записей коллекции
Следующий тестовый пример добавляет новую сущность Commit в наш репозиторий :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
LOGGER.info( "Adding invalidates Collection Cache" ); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository. class , 1L); assertEquals( 2 , repository.getCommits().size()); Commit commit = new Commit(); commit.getChanges().add( new Change( "Main.java" , "0b3,17..." ) ); repository.addCommit(commit); }); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository. class , 1L); assertEquals( 3 , repository.getCommits().size()); }); |
Запуск этого теста приводит к следующему выводу:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
--Adding invalidates Collection Cache insert into commit (id, repository_id, review) values ( default , 1, false ) insert into commit_change (commit_id, index_id, diff, path) values (3, 0, '0b3,17...' , 'Main.java' ) --committed JDBC Connection select commits0_.repository_id as reposito3_0_0_, commits0_.id as id1_1_0_, commits0_.id as id11_1_1_, commits0_.repository_id as reposito3_1_1_, commits0_.review as review2_1_1_ from commit commits0_ where commits0_.repository_id=1 --committed JDBC Connection |
После того, как новый объект Commit сохраняется, кэш коллекции Repository.commits очищается, а связанные объекты Commits выбираются из базы данных (при следующем обращении к коллекции).
Удаление существующих записей Коллекции
Удаление элемента Collection выполняется по той же схеме:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
LOGGER.info( "Removing invalidates Collection Cache" ); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository. class , 1L); assertEquals( 2 , repository.getCommits().size()); Commit removable = repository.getCommits().get( 0 ); repository.removeCommit(removable); }); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository. class , 1L); assertEquals( 1 , repository.getCommits().size()); }); |
Получается следующий вывод:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
--Removing invalidates Collection Cache delete from commit_change where commit_id=1 delete from commit where id=1 --committed JDBC Connection select commits0_.repository_id as reposito3_0_0_, commits0_.id as id1_1_0_, commits0_.id as id1_1_1_, commits0_.repository_id as reposito3_1_1_, commits0_.review as review2_1_1_ from commit commits0_ where commits0_.repository_id=1 --committed JDBC Connection |
Кэш Коллекции удаляется после изменения его структуры.
Удаление элементов коллекции напрямую
Hibernate может обеспечить согласованность кэша, если он знает обо всех изменениях, которые претерпевает целевая кэшированная коллекция. Hibernate использует свои собственные типы Collection (например, PersistentBag , PersistentSet ) для обеспечения отложенной загрузки или обнаружения грязного состояния .
Если внутренний элемент Collection удаляется без обновления состояния Collection, Hibernate не сможет сделать недействительной текущую кэшированную запись Collection:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
LOGGER.info( "Removing Child causes inconsistencies" ); doInTransaction(session -> { Commit commit = (Commit) session.get(Commit. class , 1L); session.delete(commit); }); try { doInTransaction(session -> { Repository repository = (Repository) session.get(Repository. class , 1L); assertEquals( 1 , repository.getCommits().size()); }); } catch (ObjectNotFoundException e) { LOGGER.warn( "Object not found" , e); } |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
--Removing Child causes inconsistencies delete from commit_change where commit_id=1 delete from commit where id=1 - committed JDBC Connection select collection0_.id as id1_1_0_, collection0_.repository_id as reposito3_1_0_, collection0_.review as review2_1_0_ from commit collection0_ where collection0_.id=1 --No row with the given identifier exists: -- [CollectionCacheTest$Commit#1] --rolled JDBC Connection |
Когда объект Commit был удален, Hibernate не знал, что ему нужно обновить все связанные кэши сборов. В следующий раз, когда мы загрузим коллекцию Commit , Hibernate поймет, что некоторые сущности больше не существуют, и выдаст исключение.
Обновление элементов коллекции с использованием HQL
Hibernate может поддерживать целостность кэша при выполнении массовых обновлений через HQL:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
LOGGER.info( "Updating Child entities using HQL" ); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository. class , 1L); for (Commit commit : repository.getCommits()) { assertFalse(commit.review); } }); doInTransaction(session -> { session.createQuery( "update Commit c " + "set c.review = true " ) .executeUpdate(); }); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository. class , 1L); for (Commit commit : repository.getCommits()) { assertTrue(commit.review); } }); |
При выполнении этого теста генерируется следующий SQL:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
--Updating Child entities using HQL --committed JDBC Connection update commit set review= true --committed JDBC Connection select commits0_.repository_id as reposito3_0_0_, commits0_.id as id1_1_0_, commits0_.id as id1_1_1_, commits0_.repository_id as reposito3_1_1_, commits0_.review as review2_1_1_ from commit commits0_ where commits0_.repository_id=1 --committed JDBC Connection |
Первая транзакция не требует попадания в базу данных, а полагается только на кэш второго уровня. HQL UPDATE очищает кэш коллекции, поэтому Hibernate должен будет перезагрузить его из базы данных при последующем доступе к коллекции.
Обновление элементов коллекции с использованием SQL
Hibernate также может сделать недействительными записи в кэше для массовых операторов SQL UPDATE:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
LOGGER.info( "Updating Child entities using SQL" ); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository. class , 1L); for (Commit commit : repository.getCommits()) { assertFalse(commit.review); } }); doInTransaction(session -> { session.createSQLQuery( "update Commit c " + "set c.review = true " ) .addSynchronizedEntityClass(Commit. class ) .executeUpdate(); }); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository. class , 1L); for (Commit commit : repository.getCommits()) { assertTrue(commit.review); } }); |
Генерация следующего вывода:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
--Updating Child entities using SQL --committed JDBC Connection update commit set review= true --committed JDBC Connection select commits0_.repository_id as reposito3_0_0_, commits0_.id as id1_1_0_, commits0_.id as id1_1_1_, commits0_.repository_id as reposito3_1_1_, commits0_.review as review2_1_1_ from commit commits0_ where commits0_.repository_id=1 --committed JDBC Connection |
BulkOperationCleanupAction отвечает за очистку кэша второго уровня в массовых инструкциях DML . В то время как Hibernate может обнаруживать затронутые области кэша при выполнении оператора HQL , для собственных запросов вы должны указать Hibernate, какие области оператор должен аннулировать. Если вы не укажете такой регион, Hibernate очистит все области кэша второго уровня.
Вывод
Collection Cache — очень полезная функция, дополняющая кэш сущностей второго уровня . Таким образом, мы можем хранить весь граф сущностей, уменьшая нагрузку на запросы к базе данных в приложениях, предназначенных для чтения Как и при очистке AUTO , Hibernate не может анализировать затронутые табличные пространства при выполнении собственных запросов. Чтобы избежать проблем согласованности (при использовании автоматической очистки) или пропусков кэша (кэш второго уровня), всякий раз, когда нам нужно выполнить собственный запрос, мы должны явно объявить целевые таблицы, чтобы Hibernate мог предпринять соответствующие действия (например, очистить или аннулировать кэш). регионы).
- Код доступен на GitHub .
Ссылка: | Как работает Hibernate Collection Cache от нашего партнера по JCG Влада Михалча в блоге Влада Михалча . |