REST调用下Spring Data JPA实体部分加载缓存异常问题咨询
问题现象
在使用Spring Data JPA的Specification与Entity Graph实现实体部分加载时,出现不一致行为:
- 通过REST端点调用方法时,返回的实体仅包含部分关联数据(被缓存)
- 通过定时任务调用同一方法时,能正常加载完整关联数据
业务逻辑
需求是:加载所有包含班级1C的学校,提取ID后重新加载这些学校的完整数据。
核心代码与输出对比
关键代码片段
实体定义
@Entity @NamedEntityGraphs( value = [ NamedEntityGraph( name = "School.classes", attributeNodes = [NamedAttributeNode("classes")] ) ] ) class School { @Id @GeneratedValue var id: Long = 0 @Column(length = 30, nullable = false) var name: String = "" @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = false, mappedBy = "school") var classes: MutableSet<SchoolClass> = mutableSetOf() override fun toString(): String { return "School(id=$id, name='$name', classes=${classes.joinToString { it.name }})" } } @Entity class SchoolClass { @Id @GeneratedValue var id: Long = 0 @Column(length = 30, nullable = false) var name: String = "" @ManyToOne(optional = false) var school: School? = null override fun toString(): String { return "SchoolClass(id=$id, name='$name', school=${school?.name})" } }
Repository定义
interface SchoolRepository : JpaRepository<School, Long>, JpaSpecificationExecutor<School> { @EntityGraph("School.classes") override fun findAll(spec: Specification<School>?): List<School> @Query("select s from School s where s.id in :ids") @EntityGraph("School.classes") fun findAllByIdWithClasses(@Param("ids") ids: Set<Long>): List<School> } object SchoolSpecifications { fun className(className: String) = Specification<School> { root, _, criteriaBuilder -> val classes = root.join<School, SchoolClass>("classes") criteriaBuilder.equal(classes.get<String>("name"), className) } fun name(name: String) = Specification<School> { root, _, criteriaBuilder -> criteriaBuilder.equal(root.get<String>("name"), name) } }
业务服务与控制器
@Service class SchoolService(private val schoolRepository: SchoolRepository) { fun crazyStuff(): List<School> { val schoolIds = schoolRepository.findAll(SchoolSpecifications.className("1C")) .map { it.id } .toSet() return schoolRepository.findAllByIdWithClasses(schoolIds) } fun allFine(): List<School> { return schoolRepository.findAll(SchoolSpecifications.name("Goethe-School")) } } @RestController class Controller(private val schoolService: SchoolService) { @GetMapping("/") fun query() = printSchool() @Scheduled(fixedDelay = 5000) fun scheduled() = printSchool() private fun printSchool() { println("=crazy stuff= ${schoolService.crazyStuff()}") println("=all fine= ${schoolService.allFine()}") } }
输出对比
- REST端点输出(异常):
=crazy stuff= [School(id=1, name='Goethe-School', classes=1C)] =all fine= [School(id=1, name='Goethe-School', classes=1C)]
- 定时任务输出(正常):
=crazy stuff= [School(id=1, name='Goethe-School', classes=1D, 1B, 1C, 1A)] =all fine= [School(id=1, name='Goethe-School', classes=1A, 1C, 1B, 1D)]
原因分析
问题根源在于JPA一级缓存(EntityManager缓存)的作用:
- REST请求默认处于同一个事务上下文,共用同一个EntityManager:
- 第一次调用
findAll(SchoolSpecifications.className("1C"))时,通过join查询匹配到包含1C的School,此时返回的School实体的classes集合仅包含匹配的1C(join过滤了结果),这个不完整的实体被存入一级缓存。 - 第二次调用
findAllByIdWithClasses时,JPA直接从一级缓存中取出已存在的School实体,不会执行新查询加载完整的classes集合。
- 第一次调用
- 定时任务每次执行时,Spring会创建新的事务上下文和EntityManager,一级缓存为空:
- 两次查询都执行完整SQL,第二次查询通过Entity Graph加载School的所有关联班级,因此返回完整数据。
解决方案
以下是几种可行的解决方法,按推荐程度排序:
1. 直接查询ID而非完整实体(最优)
修改Repository,添加只查询学校ID的方法,避免将不完整的School实体存入缓存:
interface SchoolRepository : JpaRepository<School, Long>, JpaSpecificationExecutor<School> { // ... 保留原有方法 @Query("select distinct s.id from School s join s.classes c where c.name = :className") fun findSchoolIdsByClassName(@Param("className") className: String): Set<Long> }
修改crazyStuff方法:
fun crazyStuff(): List<School> { val schoolIds = schoolRepository.findSchoolIdsByClassName("1C") return schoolRepository.findAllByIdWithClasses(schoolIds) }
这种方式从根源上避免了不完整实体进入缓存,性能也更优(仅查询ID字段)。
2. 拆分事务,让两次查询处于不同EntityManager
将第一次查询封装为独立事务,使用REQUIRES_NEW传播级别,确保每次查询使用新的EntityManager:
@Service class SchoolService(private val schoolRepository: SchoolRepository) { @Transactional(propagation = Propagation.REQUIRES_NEW) fun getSchoolIdsByClassName(className: String): Set<Long> { return schoolRepository.findAll(SchoolSpecifications.className(className)) .map { it.id } .toSet() } fun crazyStuff(): List<School> { val schoolIds = getSchoolIdsByClassName("1C") return schoolRepository.findAllByIdWithClasses(schoolIds) } // ... 其他方法 }
第一次查询的EntityManager在事务结束后被销毁,缓存的不完整实体不会影响第二次查询。
3. 手动清除EntityManager缓存
在第一次查询后手动清除EntityManager的缓存,注意这会清空当前EntityManager的所有缓存数据,可能影响其他操作:
@Service class SchoolService( private val schoolRepository: SchoolRepository, private val entityManager: EntityManager ) { fun crazyStuff(): List<School> { val schools = schoolRepository.findAll(SchoolSpecifications.className("1C")) val schoolIds = schools.map { it.id }.toSet() // 清除缓存,避免后续查询复用不完整实体 entityManager.clear() return schoolRepository.findAllByIdWithClasses(schoolIds) } // ... 其他方法 }
内容的提问来源于stack exchange,提问作者Marco
相关产品推荐
相关产品推荐

