Hibernate Collection Cache如何工作
介绍
之前,我描述了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 = {[email protected]} key = {[email protected]} "1" type = {[email protected]} entityOrRoleName = {[email protected]} "com.vladmihalcea.hibernate.masterclass.laboratory.cache.CollectionCacheTest$Repository.commits" tenantId = null hashCode = 31 value = {org.hibernate.cache.ehcach[email protected]3982} value = {[email protected]} "CollectionCacheEntry[1,2]" version = null timestamp = 5858841154416640
CollectionCacheEntry存储与给定存储库实体关联的提交标识符。
由于元素类型没有标识符,因此Hibernate会存储其脱水状态。 更改可嵌入的内容缓存如下:
key = {[email protected]} "com.vladmihalcea.hibernate.masterclass.laboratory.cache.CollectionCacheTest$Commit.changes#1" key = {[email protected]} "1" type = {[email protected]} entityOrRoleName = {[email protected]} "com.vladmihalcea.hibernate.masterclass.laboratory.cache.CollectionCacheTest$Commit.changes" tenantId = null hashCode = 31 value = {org.hibernate.cache.ehcach[email protected]3971} value = {[email protected]} state = {java.io.Serializable[2]@3980} 0 = {java.lang.Object[2]@3981} 0 = {[email protected]} "0a1,5..." 1 = {[email protected]} "README.txt" 1 = {java.lang.Object[2]@3982} 0 = {[email protected]} "17c17..." 1 = {[email protected]} "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将清除所有第二级缓存区域。
结论
集合缓存是一项非常有用的功能,是对第二级实体缓存的补充。 这样,我们可以存储整个实体图,从而减少了只读应用程序中的数据库查询工作量。 像使用AUTO刷新一样 ,Hibernate在执行本机查询时无法自省受影响的表空间。 为了避免一致性问题(使用AUTO刷新时)或缓存未命中(二级缓存),每当我们需要运行本机查询时,我们都必须显式声明目标表,因此Hibernate可以采取适当的措施(例如刷新或使缓存无效)地区)。
- 代码可在GitHub上获得 。
翻译自: https://www.javacodegeeks.com/2015/05/how-does-hibernate-collection-cache-work.html