Spring Boot中MySQL关联对象的保存与查询问题排查
Spring Boot关联实体保存与查询问题排查与解决
问题背景
在Spring Boot项目中操作包含多组关联的DailyEntry实体:
- User 与 DailyEntry 为一对多关系
- DailyEntry 与 DailyMacros 为一对一关系
- DailyEntry 与 Exercise 为一对多关系
保存DailyEntry时,PUT接口报错Cannot call sendError() after the response has been committed;GET接口返回200状态码但内容为空。
实体模型定义
DailyEntry 实体
@Entity @Table(name = "daily_entry") @Setter @Getter @AllArgsConstructor @NoArgsConstructor public class DailyEntry { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "daily_entry_id", unique = true) private Long id; @Column(unique = true) private LocalDate date; private int weight; @ManyToOne @JoinColumn(name="user_id") private User user; @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "daily_macros_id", unique = true) private DailyMacros dailyMacros; @OneToMany(mappedBy = "dailyEntry", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Exercise> exercise = new ArrayList<Exercise>(); }
DailyMacros 实体
@Entity @Table(name = "daily_macros") @Setter @Getter @AllArgsConstructor @NoArgsConstructor public class DailyMacros { @Id @GeneratedValue @Column(name = "daily_macros_id") private Long id; private int calories; private int protein; private int fat; private int carbs; @OneToOne(mappedBy = "dailyMacros", cascade = CascadeType.ALL, orphanRemoval = true) private DailyEntry dailyEntry; }
Exercise 实体
@Setter @Getter @Entity @AllArgsConstructor @NoArgsConstructor @Table(name= "exercises") public class Exercise { @Id @GeneratedValue @Column(name = "exercise_id") private Long id; private String name; private int sets; private int reps; private int weight; @ManyToOne @JoinColumn(name = "daily_entry_id", unique = true) private DailyEntry dailyEntry; }
User 实体
@AllArgsConstructor @NoArgsConstructor @Getter @Setter @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") private Long id; @Column(nullable = false) private String firstName; @Column(nullable = false) private String lastName; private String role = "user"; private String token; @Column(unique = true, nullable = false) protected String username; @Column(unique = true ,nullable = false) private String emailAddress; @Column(nullable = false) private String password; @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "macros_goal_id") private MacrosGoal macrosGoal; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<DailyEntry> dailyEntry = new ArrayList<>(); }
Controller 与 Service 代码
DailyEntryController
@RequestMapping(path = "/api/v1/") @RequiredArgsConstructor public class DailyEntryController { @Autowired DailyEntryService dailyEntryService; @GetMapping("/getDailyEntry") public ResponseEntity<DailyEntry> getDailyEntry(@RequestParam("username") String username, @RequestParam String date) throws ResponseStatusException { DailyEntry dailyEntry = dailyEntryService.getDailyEntry(username, date); return new ResponseEntity<>(dailyEntry, HttpStatus.OK); } @PutMapping("/addDailyEntry") public ResponseEntity<DailyEntry> addDailyEntry(@RequestBody DailyEntry dailyEntry, @RequestParam("username") String username) throws ResponseStatusException { DailyEntry dailyEntryInfo = dailyEntryService.createDailyEntry(dailyEntry, username); return new ResponseEntity<>( dailyEntryInfo, HttpStatus.OK); } }
DailyEntryServiceImpl
@Slf4j @Service public class DailyEntryServiceImpl implements DailyEntryService { @Autowired DailyEntryRepository dailyEntryRepository; @Autowired ExerciseRepository exerciseRepository; @Autowired UserRepository userRepository; @Autowired DailyMacrosRepository dailyMacrosRepository; @Override public DailyEntry addExercise(Exercise exercise) { return null; } @Override public DailyEntry getDailyEntry(String username, String date) { DailyEntry entry = null; Optional<User> userFromDatabase = userRepository.findByUsername(username); User user = userFromDatabase.get(); LocalDate localDate = LocalDate.parse(date); List<DailyEntry> dailyEntryList = user.getDailyEntry(); for(DailyEntry e : dailyEntryList) { if(e.getDate() == localDate){ entry = e; } } return entry; } @Override public DailyEntry createDailyEntry(DailyEntry dailyEntry, String username) { DailyEntry entry = new DailyEntry(); Optional<User> userFromDatabase = userRepository.findByUsername(username); User user = userFromDatabase.get(); entry.setUser(user); entry.setDate(LocalDate.parse(dailyEntry.getDate().toString())); entry.setDailyMacros(dailyEntry.getDailyMacros()); entry.setWeight(dailyEntry.getWeight()); entry.setExercise(dailyEntry.getExercise()); DailyMacros dailyMacros = dailyEntry.getDailyMacros(); dailyMacros.setDailyEntry(entry); dailyMacrosRepository.save(dailyMacros); List<Exercise> exercise = dailyEntry.getExercise(); for (Exercise e : exercise) { e.setDailyEntry(entry); exerciseRepository.save(e); } List<DailyEntry> dailyEntryList = user.getDailyEntry(); dailyEntryList.add(entry); user.setDailyEntry(dailyEntryList); userRepository.save(user); return entry; } }
问题分析与修复方案
1. 实体关联配置错误
- Exercise 实体:
@JoinColumn(name = "daily_entry_id", unique = true)中的unique=true违反一对多关系,一个DailyEntry应该可以关联多个Exercise,需删除该属性。 - DailyMacros 实体:双向一对一关联中,DailyEntry是主控方(已配置
cascade=CascadeType.ALL),DailyMacros的@OneToOne无需重复配置cascade=CascadeType.ALL,否则会导致级联保存冲突,需删除该配置。
2. 保存逻辑冗余与错误
- 无需手动保存DailyMacros和Exercise,User的
@OneToMany已配置cascade=CascadeType.ALL,通过保存User即可级联保存所有关联对象,手动保存会触发重复操作,引发响应提交错误。 - 无需新建DailyEntry对象,直接复用传入的对象并设置双向关联即可。
3. 查询逻辑错误
- 日期比较使用
==是引用比较,LocalDate是对象,需改用equals()方法,否则永远匹配不到数据,导致返回空内容。 - User的dailyEntry是懒加载集合,Service方法需添加
@Transactional注解,否则会触发懒加载异常或无法正确获取数据。
4. HTTP方法误用
- 新增资源应使用
@PostMapping而非@PutMapping,Put用于更新操作,Post用于创建操作。
修复后代码示例
修复后的Exercise实体
@Setter @Getter @Entity @AllArgsConstructor @NoArgsConstructor @Table(name= "exercises") public class Exercise { @Id @GeneratedValue @Column(name = "exercise_id") private Long id; private String name; private int sets; private int reps; private int weight; @ManyToOne @JoinColumn(name = "daily_entry_id") private DailyEntry dailyEntry; }
修复后的DailyMacros实体
@Entity @Table(name = "daily_macros") @Setter @Getter @AllArgsConstructor @NoArgsConstructor public class DailyMacros { @Id @GeneratedValue @Column(name = "daily_macros_id") private Long id; private int calories; private int protein; private int fat; private int carbs; @OneToOne(mappedBy = "dailyMacros", orphanRemoval = true) private DailyEntry dailyEntry; }
修复后的DailyEntryServiceImpl
@Slf4j @Service @Transactional public class DailyEntryServiceImpl implements DailyEntryService { @Autowired UserRepository userRepository; @Override public DailyEntry addExercise(Exercise exercise) { return null; } @Override public DailyEntry getDailyEntry(String username, String date) { Optional<User> userFromDatabase = userRepository.findByUsername(username); if(userFromDatabase.isEmpty()){ throw new ResponseStatusException(HttpStatus.NOT_FOUND, "用户不存在"); } User user = userFromDatabase.get(); LocalDate localDate = LocalDate.parse(date); return user.getDailyEntry().stream() .filter(e -> e.getDate().equals(localDate)) .findFirst() .orElse(null); } @Override public DailyEntry createDailyEntry(DailyEntry dailyEntry, String username) { Optional<User> userFromDatabase = userRepository.findByUsername(username); if(userFromDatabase.isEmpty()){ throw new ResponseStatusException(HttpStatus.NOT_FOUND, "用户不存在"); } User user = userFromDatabase.get(); // 设置双向关联 dailyEntry.setUser(user); if(dailyEntry.getDailyMacros() != null){ dailyEntry.getDailyMacros().setDailyEntry(dailyEntry); } if(dailyEntry.getExercise() != null){ dailyEntry.getExercise().forEach(exercise -> exercise.setDailyEntry(dailyEntry)); } user.getDailyEntry().add(dailyEntry); userRepository.save(user); return dailyEntry; } }
修复后的DailyEntryController
@RequestMapping(path = "/api/v1/") @RequiredArgsConstructor @RestController public class DailyEntryController { @Autowired DailyEntryService dailyEntryService; @GetMapping("/getDailyEntry") public ResponseEntity<DailyEntry> getDailyEntry(@RequestParam("username") String username, @RequestParam String date) { DailyEntry dailyEntry = dailyEntryService.getDailyEntry(username, date); return dailyEntry == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(dailyEntry); } @PostMapping("/addDailyEntry") public ResponseEntity<DailyEntry> addDailyEntry(@RequestBody DailyEntry dailyEntry, @RequestParam("username") String username) { DailyEntry dailyEntryInfo = dailyEntryService.createDailyEntry(dailyEntry, username); return ResponseEntity.ok(dailyEntryInfo); } }
额外注意事项
- 所有Repository需继承
JpaRepository,例如:public interface DailyEntryRepository extends JpaRepository<DailyEntry, Long> - 双向关联需处理序列化循环引用,可添加
@JsonManagedReference和@JsonBackReference注解:- 在主控方的关联字段添加
@JsonManagedReference - 在被控方的关联字段添加
@JsonBackReference
- 在主控方的关联字段添加
内容的提问来源于stack exchange,提问作者Jason
相关产品推荐
相关产品推荐

