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

Posted

技术标签:

【中文标题】如何在不使用查询缓存的情况下缓存 Spring Data JPA 查询方法的结果?【英文标题】:How to cache results of a Spring Data JPA query method without using query cache? 【发布时间】:2014-12-02 06:18:37 【问题描述】:

我有一个带有 Spring Data JPA(休眠后端)存储库类的 Spring Boot 应用程序。我添加了几个自定义查找器方法,其中一些带有特定的@Query 注释来告诉它如何获取数据。我已经为休眠二级缓存设置了 EhCache,但到目前为止,我可以获得这些结果缓存的唯一方法是启用休眠查询缓存。我更愿意定义一个特定的缓存并将实际的域对象存储在那里,就像它是一个普通的查找器一样。以下是我的回购代码:

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,那么这些数据会被缓存在那里并且休眠不会执行查询。但是当我禁用查询缓存时,它不会被缓存。我在这里做错了什么?请帮忙!

【问题讨论】:

为什么@Cache 注释应该为非实体做任何事情?该注释适用于不在任意类或接口上的实体。 我正试图弄清楚...所以任何帮助将不胜感激。由 Spring 在运行时创建的 PagingAndSortingRepository 中指定的其他 find... 方法使用 JPA/Hibernate 的二级缓存提供缓存。这工作正常。但是我创建的这个查找器方法,我不知道如何让 IT 缓存... 我怀疑 findAll 是否正在缓存,除非您在实体上缓存注释(这是它们应该去的地方)。如果您正在尝试(或期望)缓存不可缓存的实体,它将无法工作,只有查询缓存才能在这种情况下工作。 谢谢。这就是我想出来的。我一直在尝试使用实体的休眠二级缓存来缓存自定义查询。我不想使用查询缓存,所以我现在使用 Spring 的缓存抽象,并在这些自定义查找器的服务级别缓存。 【参考方案1】:

您的代码不起作用的原因是@Cache 不打算以这种方式工作。如果要缓存某个查询方法执行的结果,最简单的方法就是使用Spring的caching abstraction。

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(有关详细信息,请参阅reference documentation)以插入您喜欢的任何缓存解决方案(在此处使用普通的ConcurrentHashMap)。

 @Configuration
 @EnableCaching
 class CachingConfig 

   @Bean
   CacheManager cacheManager() 

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

     return cacheManager;
   
 

【讨论】:

谢谢。我确实开始走这条路,但是当我在一个类中有多个方法时遇到了一个问题,我需要围绕这些方法进行缓存抽象,但实际上是从同一个类中的另一个方法调用的。我需要走 AspectJ 路线。事实证明,我现在出于业务原因放弃了缓存这些数据的需要,所以这个“问题”不再是问题。但你是对的,最好的路线肯定是转向 Spring 的 Cache 抽象。谢谢! 如果我们将实体缓存在 Spring Cache 中,难道不会导致各种问题和奇怪的行为吗? Spring 建议仅注释具体类(“方法可见性和缓存注释”之后的docs.spring.io/spring/docs/current/spring-framework-reference/… 段落),我在接口上使用它时遇到此错误:“无法生成类的 CGLIB 子类 [类 com.sun.proxy.$Proxy180]"。处理这种情况的推荐方法是什么? (这是 Spring 4.1.7) &lt;S extends PromotionServiceXref&gt; S save(S service); 上使用@CachePut 不是更好吗?此外,您对@Cacheable 的使用将导致在中型到大型应用程序中产生数百或数千个不同的缓存。这是可取的吗?我本来期望例如@Cacheable("services") 与自定义键组合。 我尝试使用这个解决方案,但是很明显,当从缓存中拉出时,缓存的实体会分离。这可能是延迟加载集合的问题。【参考方案2】:

您需要注意,通过放弃 Hibernate QueryCache,您有责任使在保存、更新、删除影响查询结果的实体时变得陈旧的查询无效(Oliver 通过在保存时设置 CacheEvict 所做的事情)-我认为这可能是一种痛苦——或者至少你需要考虑并忽略它,如果它对你的场景来说不是一个真正的问题。

【讨论】:

【参考方案3】:

首先我引用你的问题:

我做错了什么?

您尝试命名缓存的方式不适合休眠将如何使用它。检查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 是的,如果您真的想使用我的 shell 脚本,请运行 mvn clean install 或更改我的 env.sh。然后检查代表 3x imageRepository.count() 调用的 sql 查询数。

【讨论】:

以上是关于如何在不使用查询缓存的情况下缓存 Spring Data JPA 查询方法的结果?的主要内容,如果未能解决你的问题,请参考以下文章

如何在不接触缓存的情况下写入或读取内存

如何在不丢失“this”上下文的情况下从 React 组件中写入 apollo 缓存

在不使用 windows 文件缓存的情况下复制文件

ccache 在不删除 CMake 文件的情况下不会命中缓存

mybatis0209 二级缓存

使用 Apollo 链路状态缓存实现客户端过滤