Spring Boot项目中Query Cache与Specification配合失效问题咨询
看来你在Spring Boot + Spring Data JPA + Hibernate + Ehcache的架构下遇到了二级缓存的特定场景失效问题,我来帮你分析原因并给出针对性解决方案:
一、HQL查询结果用Page<>包装时缓存失效
问题原因
当用Page<>包装HQL查询结果时,Spring Data JPA会自动执行两个独立查询:
- 查询当前页的数据列表
- 查询总记录数的count统计查询
即使你给数据列表的HQL设置了query.setHint("org.hibernate.cacheable", true),count查询默认不会启用缓存,这就导致整体分页查询无法完全利用二级缓存;另外分页参数(页码、每页条数)会成为缓存key的一部分,若未正确配置缓存区域,也可能导致缓存无法命中。
解决方案
手动为count查询添加缓存hint
如果是手动构建HQL查询,需要同时给数据列表查询和count查询都开启缓存:// 数据列表查询 Query listQuery = em.createQuery("SELECT c FROM City c WHERE c.province = :province", City.class); listQuery.setParameter("province", "Guangdong"); listQuery.setHint("org.hibernate.cacheable", true); listQuery.setHint("org.hibernate.cacheRegion", "cityList"); // count统计查询 Query countQuery = em.createQuery("SELECT COUNT(c) FROM City c WHERE c.province = :province"); countQuery.setParameter("province", "Guangdong"); countQuery.setHint("org.hibernate.cacheable", true); countQuery.setHint("org.hibernate.cacheRegion", "cityCount"); long total = (long) countQuery.getSingleResult(); List<City> content = listQuery.setFirstResult(pageable.getOffset()).setMaxResults(pageable.getPageSize()).getResultList(); Page<City> page = new PageImpl<>(content, pageable, total);使用
@Query注解时配置全局缓存提示
如果你用Spring Data的@Query注解实现分页,可以通过countQuery指定统计查询,并结合@QueryHints为两个查询同时开启缓存:@Query(value = "SELECT c FROM City c WHERE c.province = :province", countQuery = "SELECT COUNT(c) FROM City c WHERE c.province = :province") @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value = "true"), @QueryHint(name = "org.hibernate.cacheRegion", value = "cityList") }) Page<City> findByProvince(@Param("province") String province, Pageable pageable);这里的
@QueryHints会同时作用于主查询和count查询,确保两者都能利用二级缓存。
二、使用Specification和Criteria查询时缓存失效
问题原因
Spring Data JPA的Specification机制默认不会为构建的CriteriaQuery添加Hibernate缓存hint,Hibernate无法识别该查询需要使用二级缓存,自然不会触发缓存读写操作。
解决方案
你需要自定义Repository实现,手动为Criteria查询添加缓存hint,或者全局扩展SimpleJpaRepository让所有Specification查询自动启用缓存:
自定义Repository实现类
先定义自定义Repository接口:public interface CityCustomRepository { Page<City> findBySpecification(Specification<City> spec, Pageable pageable); }然后实现该接口,在构建查询时添加缓存hint:
@Repository public class CityCustomRepositoryImpl implements CityCustomRepository { @PersistenceContext private EntityManager em; @Override public Page<City> findBySpecification(Specification<City> spec, Pageable pageable) { // 构建数据列表查询 CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<City> cq = cb.createQuery(City.class); Root<City> root = cq.from(City.class); cq.where(spec.toPredicate(root, cq, cb)); TypedQuery<City> listQuery = em.createQuery(cq); listQuery.setHint("org.hibernate.cacheable", true); listQuery.setHint("org.hibernate.cacheRegion", "cityList"); listQuery.setFirstResult(pageable.getOffset()).setMaxResults(pageable.getPageSize()); List<City> content = listQuery.getResultList(); // 构建count统计查询并添加缓存hint CriteriaQuery<Long> countCq = cb.createQuery(Long.class); Root<City> countRoot = countCq.from(City.class); countCq.select(cb.count(countRoot)).where(spec.toPredicate(countRoot, countCq, cb)); TypedQuery<Long> countQuery = em.createQuery(countCq); countQuery.setHint("org.hibernate.cacheable", true); countQuery.setHint("org.hibernate.cacheRegion", "cityCount"); long total = countQuery.getSingleResult(); return new PageImpl<>(content, pageable, total); } }最后让你的JpaRepository继承自定义接口:
public interface CityRepository extends JpaRepository<City, Long>, CityCustomRepository { }全局扩展SimpleJpaRepository(可选)
如果你希望所有Specification查询都自动启用缓存,可以扩展SimpleJpaRepository,重写相关方法统一添加缓存hint:public class CustomSimpleJpaRepository<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> { private final EntityManager em; public CustomSimpleJpaRepository(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) { super(entityInformation, entityManager); this.em = entityManager; } @Override protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) { TypedQuery<S> query = super.getQuery(spec, domainClass, sort); query.setHint("org.hibernate.cacheable", true); query.setHint("org.hibernate.cacheRegion", domainClass.getSimpleName() + "List"); return query; } @Override protected TypedQuery<Long> getCountQuery(Specification<T> spec) { TypedQuery<Long> query = super.getCountQuery(spec); query.setHint("org.hibernate.cacheable", true); query.setHint("org.hibernate.cacheRegion", getDomainClass().getSimpleName() + "Count"); return query; } }然后配置Spring Data使用这个自定义的Repository基类:
@Configuration @EnableJpaRepositories(repositoryBaseClass = CustomSimpleJpaRepository.class) public class JpaConfig { }
额外注意事项
- 确保你的
City实体类已经正确配置Hibernate二级缓存,比如添加@Cache注解:@Entity @Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "city") public class City { // 实体字段... } - 确认Ehcache配置文件中已经定义了对应的缓存区域(比如
cityList、cityCount、city),并设置合理的过期时间和最大容量。
内容的提问来源于stack exchange,提问作者Maxi Wu

