介绍
之前,我描述了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());}
});
并且首次访问集合时将对其进行缓存:
selectcollection0_.id as id1_0_0_,collection0_.name as name2_0_0_
fromRepository collection0_
wherecollection0_.id=1 selectcommits0_.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_
fromcommit commits0_
wherecommits0_.r selectchanges0_.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_
fromcommit_change changes0_
wherechanges0_.commit_id=1 selectchanges0_.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_
fromcommit_change changes0_
wherechanges0_.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 = nullhashCode = 31
value = {org.hibernate.cache.ehcache.internal.strategy.AbstractReadWriteEhcacheAccessStrategy$Item@3982} value = {org.hibernate.cache.spi.entry.CollectionCacheEntry@3986} "CollectionCacheEntry[1,2]"version = nulltimestamp = 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 = nullhashCode = 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 = nulltimestamp = 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 Cacheinsert
intocommit(id, repository_id, review)
values(default, 1, false)insert
intocommit_change(commit_id, index_id, diff, path)
values(3, 0, '0b3,17...', 'Main.java')--committed JDBC Connectionselectcommits0_.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_
fromcommit commits0_
wherecommits0_.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 Cachedelete
fromcommit_change
wherecommit_id=1delete
fromcommit
whereid=1--committed JDBC Connectionselectcommits0_.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_
fromcommit commits0_
wherecommits0_.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 inconsistenciesdelete
fromcommit_change
wherecommit_id=1delete
fromcommit
whereid=1-committed JDBC Connectionselectcollection0_.id as id1_1_0_,collection0_.repository_id as reposito3_1_0_,collection0_.review as review2_1_0_
fromcommit collection0_
wherecollection0_.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 Connectionupdatecommit
setreview=true--committed JDBC Connectionselectcommits0_.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_
fromcommit commits0_
wherecommits0_.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 Connectionupdatecommit
setreview=true--committed JDBC Connectionselectcommits0_.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_
fromcommit commits0_
wherecommits0_.repository_id=1 --committed JDBC Connection
BulkOperationCleanupAction负责清理大容量DML语句上的二级缓存。 尽管Hibernate在执行HQL语句时可以检测到受影响的缓存区域,但是对于本机查询,您需要指示Hibernate该语句应使哪些区域无效。 如果您未指定任何此类区域,则Hibernate将清除所有第二级缓存区域。
结论
集合缓存是一项非常有用的功能,是对第二级实体缓存的补充。 这样,我们可以存储整个实体图,从而减少了只读应用程序中的数据库查询工作量。 像使用AUTO刷新一样 ,Hibernate在执行本机查询时无法自省受影响的表空间。 为了避免一致性问题(使用AUTO刷新时)或缓存未命中(二级缓存),每当我们需要运行本机查询时,我们都必须显式声明目标表,因此Hibernate可以采取适当的措施(例如刷新或使缓存无效)地区)。
- 代码可在GitHub上获得 。
翻译自: https://www.javacodegeeks.com/2015/05/how-does-hibernate-collection-cache-work.html