Spring Data JPA findById返回缓存值而非数据库值如何解决?
问题
我在为已搭建的API开发通知系统,通过切面监听ProjectRepository.save()方法,意图对比save方法参数中Project实体的状态与数据库原状态。但调用findById查询时返回的是缓存值——即便数据库中仍是旧状态,返回的对象状态也始终与save方法的参数对象一致。
切面代码如下:
@Aspect @Component @RequiredArgsConstructor public class NotificationAspect { private final UserService userService; private final ProjectRepository projectRepository; private final NotificationService notificationService; @Pointcut("execution(* com.stars.domain.project.ProjectRepository.save(..))") public void projectSavePointcut() {} @Before("projectSavePointcut()") public void sendNotificationOnStatusChange(JoinPoint joinPoint) { if(joinPoint.getArgs().length > 0 && joinPoint.getArgs()[0] instanceof Project) { Project projectToUpdate = (Project) joinPoint.getArgs()[0]; Optional<Project> oldProject = projectRepository.findById(projectToUpdate.getProjectId()); if(oldProject.isPresent() && !oldProject.get().getStatus().equals(projectToUpdate.getStatus())) { notificationService.saveNotification( MessageFormat.format("Project: {} status has been changed from: {} to: {}", projectToUpdate.getName(), oldProject.get().getStatus(), projectToUpdate.getStatus()), List.of(userService.getUser(projectToUpdate.getCreatedBy()))); } } } }
即使数据库中记录的状态不同,以下代码行始终返回true:
oldProject.get().getStatus().equals(projectToUpdate.getStatus())
解决方案
1. 分离实体后查询数据库
Spring Data JPA的EntityManager会缓存已加载的实体,导致你查询到的oldProject和projectToUpdate是同一个实例。可以通过EntityManager.detach()将待更新实体从缓存中分离,再查询就能获取数据库中的真实旧数据。
修改后的切面代码:
@Aspect @Component @RequiredArgsConstructor public class NotificationAspect { private final UserService userService; private final ProjectRepository projectRepository; private final NotificationService notificationService; private final EntityManager entityManager; @Pointcut("execution(* com.stars.domain.project.ProjectRepository.save(..))") public void projectSavePointcut() {} @Before("projectSavePointcut()") public void sendNotificationOnStatusChange(JoinPoint joinPoint) { if(joinPoint.getArgs().length > 0 && joinPoint.getArgs()[0] instanceof Project) { Project projectToUpdate = (Project) joinPoint.getArgs()[0]; // 从持久化上下文分离实体,避免缓存干扰 entityManager.detach(projectToUpdate); // 查询数据库获取真实旧状态 Optional<Project> oldProject = projectRepository.findById(projectToUpdate.getProjectId()); if(oldProject.isPresent() && !oldProject.get().getStatus().equals(projectToUpdate.getStatus())) { notificationService.saveNotification( MessageFormat.format("Project: {} status has been changed from: {} to: {}", projectToUpdate.getName(), oldProject.get().getStatus(), projectToUpdate.getStatus()), List.of(userService.getUser(projectToUpdate.getCreatedBy()))); } // 将实体重新关联到上下文,不影响后续save操作 entityManager.merge(projectToUpdate); } } }
2. 直接查询目标字段(绕开实体缓存)
通过JPQL或原生SQL只查询需要对比的状态字段,绕开实体缓存机制,直接获取数据库中的原始值。
首先在ProjectRepository中新增查询方法:
public interface ProjectRepository extends JpaRepository<Project, Long> { @Query("select p.status from Project p where p.projectId = :projectId") Optional<String> findStatusById(@Param("projectId") Long projectId); }
然后修改切面中的对比逻辑:
@Before("projectSavePointcut()") public void sendNotificationOnStatusChange(JoinPoint joinPoint) { if(joinPoint.getArgs().length > 0 && joinPoint.getArgs()[0] instanceof Project) { Project projectToUpdate = (Project) joinPoint.getArgs()[0]; // 直接查询数据库中的旧状态 Optional<String> oldStatus = projectRepository.findStatusById(projectToUpdate.getProjectId()); if(oldStatus.isPresent() && !oldStatus.get().equals(projectToUpdate.getStatus())) { notificationService.saveNotification( MessageFormat.format("Project: {} status has been changed from: {} to: {}", projectToUpdate.getName(), oldStatus.get(), projectToUpdate.getStatus()), List.of(userService.getUser(projectToUpdate.getCreatedBy()))); } } }
3. 调整切面时机(备选方案)
若业务允许,可将切面通知时机从@Before改为@AfterCommit,但需在@Before阶段提前保存旧状态。此方案适用于不需要在save操作执行前做对比的场景,示例如下:
@Aspect @Component @RequiredArgsConstructor public class NotificationAspect { private final UserService userService; private final ProjectRepository projectRepository; private final NotificationService notificationService; // 用ThreadLocal存储当前线程的旧状态,避免多线程干扰 private final ThreadLocal<String> oldStatusHolder = new ThreadLocal<>(); private final EntityManager entityManager; @Before("projectSavePointcut()") public void recordOldStatus(JoinPoint joinPoint) { if(joinPoint.getArgs().length > 0 && joinPoint.getArgs()[0] instanceof Project) { Project projectToUpdate = (Project) joinPoint.getArgs()[0]; // 分离实体后查询旧状态 entityManager.detach(projectToUpdate); Optional<Project> oldProject = projectRepository.findById(projectToUpdate.getProjectId()); oldProject.ifPresent(p -> oldStatusHolder.set(p.getStatus())); entityManager.merge(projectToUpdate); } } @AfterReturning("projectSavePointcut()") public void sendNotificationOnStatusChange(JoinPoint joinPoint) { if(joinPoint.getArgs().length > 0 && joinPoint.getArgs()[0] instanceof Project) { Project projectToUpdate = (Project) joinPoint.getArgs()[0]; String oldStatus = oldStatusHolder.get(); if(oldStatus != null && !oldStatus.equals(projectToUpdate.getStatus())) { notificationService.saveNotification( MessageFormat.format("Project: {} status has been changed from: {} to: {}", projectToUpdate.getName(), oldStatus, projectToUpdate.getStatus()), List.of(userService.getUser(projectToUpdate.getCreatedBy()))); } // 清理ThreadLocal,避免内存泄漏 oldStatusHolder.remove(); } } }
内容的提问来源于stack exchange,提问作者Rafał Sokalski
相关产品推荐
相关产品推荐

