Статьи

Как работает Hibernate Collection Cache

Вступление

Ранее я описал структуру записи кэша второго уровня , которую Hibernate использует для хранения сущностей. Помимо сущностей, Hibernate также может хранить ассоциации сущностей, и эта статья расскажет о внутренней работе кэширования коллекций.

Доменная модель

Для предстоящих тестов мы будем использовать следующую модель сущности:

collectioncacherepositorycommitchange

Репозиторий имеет коллекцию объектов 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 :

Для кэшированных данных коллекции все действия по изменению фактически просто делают недействительными записи.

Исходя из текущей стратегии параллелизма, запись в кэше коллекции удаляется:

Добавление новых записей коллекции

Следующий тестовый пример добавляет новую сущность 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 .