如何在不使用查询缓存的情况下缓存Spring Data JPA查询方法的结果?

31

我有一个使用Spring Data JPA的Spring Boot应用程序(使用hibernate后端)。我已经添加了一些自定义查找方法,其中一些具有特定的@Query注释以告诉它如何获取数据。我已经为Hibernate第二级缓存设置了EhCache,但到目前为止,我唯一能够获得这些结果缓存的方法是启用Hibernate查询缓存。我希望定义一个特定的缓存,并将实际的域对象存储在那里,就像它是一个普通的查找器一样。以下是我的Repo代码:

public interface PromotionServiceXrefRepository extends PagingAndSortingRepository<PromotionServiceXref, Integer> {

  @Query("SELECT psx FROM Customer c " +
         "JOIN c.customerProductPromotions cpp " +
         "JOIN cpp.productPromotion pp " +
         "JOIN pp.promotion p JOIN p.promotionServiceXrefs psx " +
         "WHERE c.customerId = ?1")
  @QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
  @Cache(usage = CacheConcurrencyStrategy.READ_ONLY, region = "promotionServiceXrefByCustomerId")
  Set<PromotionServiceXref> findByCustomerId(int customerId);
}

这里是我定义的“promotionServiceXrefByCustomerId”缓存,但它并没有被使用:

<cache name="promotionServiceXrefByCustomerId" overflowToDisk="true" diskPersistent="true"
       maxEntriesLocalHeap="3000000" eternal="true" diskSpoolBufferSizeMB="20" memoryStoreEvictionPolicy="LFU"
       transactionalMode="off" statistics="true">
</cache>

我做错了什么?如果我启用 StandardQueryCache ,那么这些数据就会被缓存在那里,Hibernate 就不会执行查询。但是当我禁用查询缓存时,这些数据就无法被缓存。我在这里做错了什么?请帮忙!


为什么 @Cache 注释对非实体类有作用?该注释应该在实体上使用,而不是任意的类或接口。 - M. Deinum
我正在努力弄清楚它...所以任何帮助都将不胜感激。PagingAndSortingRepository中指定的其他查找方法是由Spring在运行时创建的,使用JPA/Hibernate的第二级缓存进行缓存。这个方法可以正常工作。但是我创建的这个查找方法,我无法弄清楚如何使其缓存... - Kevin M
2
我怀疑 findAll 没有缓存,除非你在实体上有缓存注释(这是它们应该放置的地方)。如果你试图(或期望)将不可缓存的实体缓存起来,那么它是行不通的,只有查询缓存才能在这种情况下工作。 - M. Deinum
谢谢。这就是我想出来的。我一直在尝试使用Hibernate第二级缓存来为实体缓存自定义查询。我不想使用查询缓存,因此现在我正在使用Spring的缓存抽象,并在服务层为这些自定义搜索器进行缓存。 - Kevin M
3个回答

63
您的代码无法正常工作的原因是@Cache并不是用于这种方式的。如果您想要缓存查询方法执行的结果,最简单的方法是使用Spring的缓存抽象

interface PromotionServiceXrefRepository extends PagingAndSortingRepository<PromotionServiceXref, Integer> {

  @Query("…")
  @Cacheable("servicesByCustomerId")
  Set<PromotionServiceXref> findByCustomerId(int customerId);

  @Override
  @CacheEvict(value = "servicesByCustomerId", key = "#p0.customer.id")
  <S extends PromotionServiceXref> S save(S service);
}

这种设置将导致对findByCustomerId(…)的调用结果按客户标识进行缓存。请注意,我们在重写的save(…)方法中添加了一个@CacheEvict,以便在保存实体时清除使用查询方法生成的缓存。这可能也必须传播到delete(…)方法中。

现在,您可以继续配置专用的CacheManager(有关详细信息,请参见参考文档),以插入您喜欢的任何缓存解决方案(这里使用简单的ConcurrentHashMap)。

 @Configuration
 @EnableCaching
 class CachingConfig {

   @Bean
   CacheManager cacheManager() {

     SimpleCacheManager cacheManager = new SimpleCacheManager();
     cacheManager.addCaches(Arrays.asList(new ConcurrentMapCache("servicesByCustomerId)));

     return cacheManager;
   }
 }

12
如果我们在Spring缓存中缓存实体,这是否会导致各种问题和奇怪行为? - Kevin Wittek
2
Spring建议只注释具体类(http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html“方法可见性和缓存注释”后的段落),我在接口上使用它时遇到了这个错误:“无法生成类的CGLIB子类[class com.sun.proxy.$Proxy180]”。应该如何处理这种情况?(这是使用Spring 4.1.7) - dave
cacheManager.addCaches(Arrays.asList(new ConcurrentMapCache("servicesByCustomerId"))); 这段代码可能会让人感到困惑,因为缓存管理器的名称不一定要与可缓存方法中定义的键相同。 - khoi nguyen
1
<S extends PromotionServiceXref> S save(S service); 上使用 @CachePut 是否更好?此外,您使用的 @Cacheable 将在中大型应用程序中导致数百个或数千个不同的缓存。这是可取的吗?我原本期望使用自定义键与 @Cacheable("services") 相结合。 - Madbreaks
4
我尝试使用这个解决方案,但从缓存中提取的缓存实体被分离了,这显然是一个懒加载集合可能出现的问题。 - Teocali
显示剩余3条评论

17

您需要知道,放弃使用Hibernate QueryCache后,您需要负责使查询失效,因为在保存、更新、删除影响查询结果的实体时,查询可能会变得陈旧(这是Oliver通过设置CacheEvict来实现的)——我认为这可能会很麻烦——或者至少需要考虑和忽略它,如果这对你的情况不是真正的问题。


4

首先,我引用你的问题:

我做错了什么?

你尝试给缓存命名的方式并不适合Hibernate将要使用它的方式。请查看org.hibernate.engine.spi.CacheInitiator,它使用org.hibernate.internal.CacheImpl,后者基于:

if ( settings.isQueryCacheEnabled() ) {
    final TimestampsRegion timestampsRegion = regionFactory.buildTimestampsRegion(
            qualifyRegionName( UpdateTimestampsCache.REGION_NAME ),
            sessionFactory.getProperties()
    );
    updateTimestampsCache = new UpdateTimestampsCache( sessionFactory, timestampsRegion );
    ...
}

你需要的是 UpdateTimestampsCache.REGION_NAME(等同于 org.hibernate.cache.spi.UpdateTimestampsCache)作为缓存名称,对于查询缓存,你必须使用恰好这个缓存名称,不能使用其他名称!

以下是与你的问题相关的其他想法:

  • 移除@Cache并将缓存名称设置为org.hibernate.cache.spi.UpdateTimestampsCache将允许你的查询通过hibernate使用ehcache进行缓存(这里不涉及spring缓存抽象)
  • 设置硬编码缓存名称可能会使你不高兴,但至少你知道发生了什么
  • Balamaci Serban(下面的帖子)非常正确

以下是我的一个项目的配置示例,该示例中ehcache + @Query + @QueryHints可以正常工作(ehcache/ehcache-in-memory.xml文件):

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="in-memory" xmlns="http://ehcache.org/ehcache.xsd">
    <!--<diskStore path="java.io.tmpdir"/>-->

    <!--
        30d = 3600×24×30 = 2592000
    -->

    <cache name="org.hibernate.cache.internal.StandardQueryCache"
           maxElementsInMemory="9999" eternal="false"
           timeToIdleSeconds="2592000" timeToLiveSeconds="2592000"
           overflowToDisk="false" overflowToOffHeap="false"/>

    <cache name="org.hibernate.cache.spi.UpdateTimestampsCache"
           maxElementsInMemory="9999" eternal="true"
           overflowToDisk="false" overflowToOffHeap="false"/>

    <defaultCache maxElementsInMemory="9999" eternal="false"
                  timeToIdleSeconds="2592000" timeToLiveSeconds="2592000"
                  overflowToDisk="false" overflowToOffHeap="false"/>
</ehcache>

还有hibernate.properties文件:

hibernate.jdbc.batch_size=20
hibernate.show_sql=true
hibernate.format_sql=true
hibernate.validator.autoregister_listeners=false
hibernate.cache.use_second_level_cache=true
hibernate.cache.use_query_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory
hibernate.hbm2ddl.auto=update
net.sf.ehcache.configurationResourceName=ehcache/ehcache-in-memory.xml
hibernate.dialect=org.hibernate.dialect.H2Dialect

以下是一些适用于我所解释的pom.xml版本:

<springframework.version>5.0.6.RELEASE</springframework.version>
<spring-security.version>5.0.5.RELEASE</spring-security.version>
<spring-data-jpa.version>2.1.0.RELEASE</spring-data-jpa.version>
<hibernate.version>5.2.13.Final</hibernate.version>
<jackson-datatype-hibernate5.version>2.9.4</jackson-datatype-hibernate5.version>

完整的工作测试位于image.persistence.repositories.ImageRepositoryTest.java,其链接为: https://github.com/adrhc/photos-server/tree/how-to-cache-results-of-a-spring-data-jpa-query-method-without-using-query-cache
运行mvn clean install或更改我的env.sh文件(如果您真的想使用我的shell脚本),然后检查代表3x imageRepository.count()调用的SQL查询次数。请注意保留HTML标记。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接