更新Task时同步更新Workflow的lastModifiedDate遇栈溢出问题求助
问题
有Workflow和Task两个实体类,Task通过@ManyToOne关联Workflow。需求是当Task对象更新时,同步更新关联Workflow的lastModifiedDate字段。为避免重复代码,使用带@PreUpdate注解的TaskAuditListener实现逻辑,在监听器中调用WorkflowRepository的自定义更新方法,但调用workflowRepository.updateLastModifiedDateById()时触发java.lang.StackOverflowError异常。
疑问:
- 更新的是
Workflow对象而非Task,为何@PreUpdate会被循环触发? - 是否有更优实现方案?
相关代码
Workflow 实体
@Entity public class Workflow { @Column(name = "last_modified_date") private Date lastModifiedDate; // 其他字段和方法 }
Task 实体
@Entity @EntityListeners(TaskAuditListener.class) public class Task { @ManyToOne @JoinColumn(name = "workflow_id") private Workflow workflow; // 其他字段和方法 }
TaskAuditListener 监听器
@Component public class TaskAuditListener { private static WorkflowRepository workflowRepository; @Autowired public void TaskAuditListener(WorkflowRepository workflowRepository) { TaskAuditListener.workflowRepository = workflowRepository; } @PreUpdate void beforeUpdate(Task task) { workflowRepository.updateLastModifiedDateById(task.getWorkflow().getId(), new Date(System.currentTimeMillis())); } }
WorkflowRepository 更新方法
@Modifying @Transactional @Query("UPDATE Workflow w SET w.lastModifiedDate = :lastModifiedDate WHERE w.id = :workflowId") void updateLastModifiedDateById(String workflowId, Date lastModifiedDate);
原因分析
栈溢出的核心是事务内的实体状态同步循环:
Task的@PreUpdate监听器触发后,调用WorkflowRepository的JPQL更新方法,该操作处于同一个事务中。- Hibernate执行JPQL更新后,会检查当前持久化上下文里的关联实体——由于
Task对象仍持有被更新的Workflow实例,Hibernate会尝试同步Task的关联状态,这会再次触发Task的@PreUpdate监听器。 - 上述过程反复循环,最终导致栈溢出。
优化方案
方案1:禁用JPQL更新后的上下文刷新
在WorkflowRepository的更新方法上添加查询提示,让Hibernate执行JPQL后不立即刷新持久化上下文,打断循环:
@Modifying @Transactional @QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.FLUSH_MODE, value = "COMMIT")) @Query("UPDATE Workflow w SET w.lastModifiedDate = :lastModifiedDate WHERE w.id = :workflowId") void updateLastModifiedDateById(String workflowId, Date lastModifiedDate);
方案2:直接操作持久化实体
通过EntityManager获取Workflow实体直接修改,利用Hibernate自动同步机制,同时避免触发额外监听:
@Component public class TaskAuditListener { @PersistenceContext private EntityManager entityManager; @PreUpdate void beforeUpdate(Task task) { Workflow workflow = entityManager.find(Workflow.class, task.getWorkflow().getId()); if (workflow != null) { // 若Workflow有自己的PreUpdate监听器,可临时禁用避免连锁触发 Session session = entityManager.unwrap(Session.class); PreUpdateEventListener[] listeners = session.getEventListeners().getPreUpdateEventListeners(); session.getEventListeners().setPreUpdateEventListeners(new PreUpdateEventListener[0]); workflow.setLastModifiedDate(new Date()); // 恢复监听器 session.getEventListeners().setPreUpdateEventListeners(listeners); } } }
方案3:数据库触发器(推荐)
直接在数据库层面创建触发器,当Task表更新时自动同步Workflow的时间字段,完全绕过Java代码的事务和监听逻辑:
-- MySQL 示例 DELIMITER // CREATE TRIGGER update_workflow_last_modified AFTER UPDATE ON task FOR EACH ROW BEGIN UPDATE workflow SET last_modified_date = NOW() WHERE id = NEW.workflow_id; END // DELIMITER ;
内容的提问来源于stack exchange,提问作者Martin Kršek
相关产品推荐
相关产品推荐

