使用Hibernate实现Timesheet存在则获取、不存在则新建保存的正确方案
核心问题根因
你第一版代码出现重复保存、最终报错的核心原因踩了两个经典坑:
- Optional的求值逻辑误解:
Optional.orElse(T value)的参数是提前无条件求值的,不管findByDate是否能查到已存在的记录,后面timesheetRepository.save(new Timesheet(...))这段代码都会被强制执行。也就是说第二次请求时,就算已经查到了之前存好的Timesheet,依然会照样执行一遍插入逻辑,生成第二条重复记录。 - 事务边界缺失+数据库无约束:没有统一的事务上下文隔离查询和持久化操作,也没有给Timesheet的
date字段设置唯一索引做兜底,重复生成的脏数据积累到第三次请求时,就会因为关联集合引用冲突、外键约束异常触发服务端500错误。
你当前改造版本的问题
你写的第二版代码逻辑方向是正确的,但存在明显可优化的冗余:
- 执行完save操作后又重新调用一次
findByDate查询刚存入的记录,平白增加一次无意义的数据库IO。实际上Spring Data JPA的save方法返回值本身就是经过持久化上下文管理的实体实例,完全不需要二次查库获取。 - 依然没有从数据库层面设置唯一约束,并发场景下两个请求同时查询都未命中记录时,依然会生成重复数据。
更优雅的实现方式
可以从三层做优化,既解决当前问题,也覆盖异常场景:
- 数据库层兜底:给
Timesheet实体的date字段加上唯一索引,从根源上杜绝同一日期出现多条重复Timesheet记录的可能。 - 代码层去掉冗余逻辑,替换为懒加载的Optional处理方式,加上明确的事务边界:
@Override @Transactional public TimesheetVO getTimesheet(LocalDate date) { // 使用orElseGet实现懒求值,只有查询无结果时才会执行创建+保存逻辑 Timesheet timesheet = timesheetRepository.findByDate(date) .orElseGet(() -> { // 注意不要直接复用类成员中的共享tasks集合,避免持久化上下文关联同一引用造成脏写 List<Task> instanceTasks = tasks.stream() .map(Task::new) .collect(Collectors.toList()); // 直接使用save的返回值作为结果,无需二次查询 return timesheetRepository.save(new Timesheet(date, 8, instanceTasks)); }); List<TaskVO> taskVOS = timesheet.getTasks().stream() .map(taskMapper::transformToVO) .collect(Collectors.toList()); TimesheetVO timesheetVO = timesheetMapper.transformToVO(timesheet); timesheetVO.setTasks(taskVOS); return timesheetVO; }
- 并发场景兜底:就算加了唯一约束,遇到两个请求同时查询未命中的极端并发情况,可以在save阶段捕获唯一键冲突异常,捕获后重新执行一次查询取已被其他请求存入的记录返回,不会影响接口可用性。
内容的提问来源于stack exchange,提问作者aratata
相关产品推荐
相关产品推荐

