hibernate
介绍
之前,我描述了Hibernate用于存储实体的二级缓存条目结构。 除了实体,Hibernate还可以存储实体关联,本文将阐明集合缓存的内部工作原理。
领域模型
对于即将进行的测试,我们将使用以下实体模型:
存储库具有一组Commit实体:
@org.hibernate.annotations.Cache(
usage = CacheConcurrencyStrategy.READ_WRITE
)
@OneToMany(mappedBy = "repository",
cascade = CascadeType.ALL, orphanRemoval = true)
private List<Commit> commits = new ArrayList<>();
每个Commit实体都有一个Change可嵌入元素的集合。
@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<>();
现在,我们将插入一些测试数据:
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
集合缓存条目结构
对于实体集合,Hibernate仅存储实体标识符,因此也需要缓存实体:
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 Collection Cache如何处理实体状态更改。
CollectionUpdateAction负责所有Collection的修改,并且只要集合发生更改,就会将关联的缓存条目逐出:
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实体:
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
保留新的Commit实体后,将清除Repository.commits集合缓存,并从数据库中获取关联的Commits实体(下次访问该集合)。
删除现有的集合条目
删除Collection元素的方式相同:
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知道目标缓存集合要进行的所有更改,它就可以确保缓存的一致性。 Hibernate使用其自己的Collection类型(例如PersistentBag , PersistentSet )来允许延迟加载或检测脏状态。
如果删除内部Collection元素而不更新Collection状态,则Hibernate将无法使当前缓存的Collection条目无效:
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不知道它必须更新所有关联的Collection Cache。 下次加载Commit集合时,Hibernate将意识到某些实体不再存在,并且将引发异常。
使用HQL更新Collection元素
通过HQL执行批量更新时,Hibernate可以保持缓存一致性:
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 UPDATE清除了集合缓存,因此,在随后访问集合时,Hibernate将不得不从数据库中重新加载它。
使用SQL更新Collection元素
Hibernate还可以使批量SQL UPDATE语句的缓存条目无效:
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负责清理大容量DML语句上的二级缓存。 尽管Hibernate在执行HQL语句时可以检测到受影响的缓存区域,但是对于本机查询,您需要指示Hibernate该语句应使哪些区域无效。 如果您未指定任何此类区域,则Hibernate将清除所有第二级缓存区域。
结论
集合缓存是一项非常有用的功能,是对第二级实体缓存的补充。 这样,我们可以存储整个实体图,从而减少了只读应用程序中的数据库查询工作量。 与自动刷新一样,Hibernate在执行本机查询时无法自省受影响的表空间。 为了避免一致性问题(使用AUTO刷新时)或缓存未命中(二级缓存),每当我们需要运行本机查询时,我们都必须明确声明目标表,以便Hibernate可以采取适当的操作(例如,刷新或使缓存无效)地区)。
- 代码可在GitHub上获得。
翻译自: https://www.javacodegeeks.com/2015/05/how-does-hibernate-collection-cache-work.html
hibernate