Рубрики
Без рубрики

Как работает кэш коллекции Hibernate

Автор оригинала: Vlad Mihalcea.

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

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

Репозиторий содержит коллекцию Фиксации сущностей:

@org.hibernate.annotations.Cache(
    usage = CacheConcurrencyStrategy.READ_WRITE
)
@OneToMany(mappedBy = "repository", 
    cascade = CascadeType.ALL, orphanRemoval = true)
private List commits = new ArrayList<>();

Каждая Фиксация сущность имеет коллекцию Изменяемых встраиваемых элементов.

@ElementCollection
@CollectionTable(
    name="commit_change",
    joinColumns = @JoinColumn(name="commit_id")
)
@org.hibernate.annotations.Cache(
    usage = CacheConcurrencyStrategy.READ_WRITE
)
@OrderColumn(name = "index_id")
private List changes = new ArrayList<>();

А теперь мы вставим некоторые тестовые данные:

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);
});

Кэш коллекции использует стратегию сквозной синхронизации для чтения:

doInTransaction(session -> {
    Repository repository = (Repository) 
        session.get(Repository.class, 1L);
    for (Commit commit : repository.getCommits()) {
        assertFalse(commit.getChanges().isEmpty());
    }
});

и коллекции кэшируются при первом доступе:

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

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

LOGGER.info("Load collections from cache");
doInTransaction(session -> {
    Repository repository = (Repository) 
        session.get(Repository.class, 1L);
    assertEquals(2, repository.getCommits().size());
});

При запуске предыдущего тестового набора не выполняется инструкция SQL SELECT :

CollectionCacheTest - Load collections from cache
JdbcTransaction - committed JDBC Connection

Для коллекций сущностей в режиме гибернации хранятся только идентификаторы сущностей, поэтому требуется, чтобы сущности также кэшировались:

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 хранит идентификаторы Фиксации , связанные с данным Репозиторием сущностью. Поскольку типы элементов не имеют идентификаторов, вместо этого Hibernate сохраняет их обезвоженное состояние. Изменение встраиваемое кэшируется следующим образом:

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 обрабатывает изменения состояния объектов.

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

protected final void evict() throws CacheException {
    if ( persister.hasCache() ) {
        final CacheKey ck = session.generateCacheKey(
            key, 
            persister.getKeyType(), 
            persister.getRole()
        );
        persister.getCacheAccessStrategy().remove( ck );
    }
}

Это поведение также задокументировано спецификацией CollectionRegionAccessStrategy :

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

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

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

Следующий тестовый случай добавляет новую Фиксацию сущности в наш Репозиторий :

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());
});

Выполнение этого теста приводит к следующим выводам:

--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

После сохранения новой Фиксации сущности кэш Repository.commits коллекции очищается, и связанные Фиксации сущности извлекаются из базы данных (при следующем доступе к коллекции).

Удаление существующих записей коллекции

Удаление элемента коллекции выполняется по той же схеме:

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());
});

Генерируется следующий вывод:

--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 может обеспечить согласованность кэша, если он знает обо всех изменениях, которым подвергается целевая коллекция cachedcollection. Hibernate использует свои собственные типы коллекций (например, PersistentBag , Постоянный набор ), чтобы разрешить ленивую загрузку или обнаружение грязного состояния .

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

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);
}
--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:

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:

--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 очищает кэш коллекции, поэтому Hibernate придется перезагрузить его из базы данных при последующем доступе к коллекции.

Обновление элементов коллекции с помощью SQL

Hibernate также может сделать недействительными записи кэша для операторов массового обновления SQL:

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);
    }
});

Создание следующего вывода:

--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 отвечает за очистку кэша второго уровня в операторах bulk DML|/. В то время как Hibernate может обнаруживать затронутые области кэша при выполнении оператора HQL , для собственных запросов вам необходимо указать Hibernate, какие области оператор должен сделать недействительными. Если вы не укажете такой регион, Hibernate очистит все области кэша второго уровня.

Кэш коллекций-очень полезная функция, дополняющая кэш сущностей второго уровня . Таким образом, мы можем хранить весь граф сущностей, уменьшая нагрузку на запросы к базе данных в приложениях, предназначенных в основном для чтения. Как и в случае с автоматической очисткой , Hibernate не может анализировать затронутые табличные пространства при выполнении собственных запросов. Чтобы избежать проблем с согласованностью (при использовании автоматической очистки) или пропусков кэша (кэш второго уровня), всякий раз, когда нам нужно выполнить собственный запрос, мы должны явно объявить целевые таблицы, чтобы Hibernate мог предпринять соответствующие действия (например, очистка или аннулирование областей кэша).

Код доступен на GitHub .