You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
    }
}

额外注意事项

  1. 所有Repository需继承JpaRepository,例如:public interface DailyEntryRepository extends JpaRepository<DailyEntry, Long>
  2. 双向关联需处理序列化循环引用,可添加@JsonManagedReference和@JsonBackReference注解:
    • 在主控方的关联字段添加@JsonManagedReference
    • 在被控方的关联字段添加@JsonBackReference

内容的提问来源于stack exchange,提问作者Jason

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 02:20:30