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

使用PostMapping同时保存父/子实体并分配外键失败排查

问题:PostMapping保存多级关联实体时外键未正确分配且出现空指针异常

问题描述

通过PostMapping同时保存父对象Course、子对象HoleLayout,以及关联两者ID的子对象Hole。目前三个实体均可保存,但子对象的外键未被正确分配,且CourseServiceImpl中出现空指针异常:

Cannot invoke "java.util.List.forEach(java.util.function.Consumer)" because the return value of "com.Rest.GolfMax.API.Models.Course.getHoleLayout()" is null

尝试过双向映射、JsonConverter等方案,问题仍未解决。

相关代码

父实体Course

@Table(name = "courses")
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private long id;

    @OneToMany(mappedBy = "course")
    private List<Score> scores;

    @OneToMany(mappedBy = "course", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<HoleLayout> holeLayout = new ArrayList<>();

    @Column(name = "courseName", nullable = false)
    private String courseName;

    // ... getters and setters

子实体HoleLayout

@Table(name = "holeLayout")
public class HoleLayout {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private long id;
    
    @OneToMany(mappedBy = "holeLayout", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Hole> holes = new ArrayList<>();

    @ManyToOne
    private Course course;

    @Enumerated(EnumType.ORDINAL)
    @Column(name = "layoutType", nullable = false)
    private LayoutType layoutType;

    @Column(name = "front9Yards", nullable = false)
    private long front9Yards;

    @Column(name = "back9Yards", nullable = false)
    private long back9Yards;

    @Column(name = "overallPar", nullable = false)
    private int overallPar;

    @Column(name = "courseRating", nullable = false)
    private double courseRating;

    @Column(name = "slopeRating", nullable = false)
    private double slopeRating;
    
    // ... getters and setters

子实体Hole

@Table(name = "hole")
public class Hole {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private long id;

    @ManyToOne
    private HoleLayout holeLayout;

    @Column(name = "holeNumber", nullable = false)
    private int holeNumber;

    @Column(name = "yards", nullable = false)
    private int yards;

    @Column(name = "par", nullable = false)
    private int par;
    
    // ... getters and setters

CourseServiceImpl代码

@Override
public Course createCourse(Course course) {
    course.getHoleLayout().forEach(layout -> layout.setCourse(course));
    
    for (int i = 0; i < course.getHoleLayout().size(); i++) {
        HOLE_REPOSITORY.save(course.getHoleLayout().get(i).getHoles().get(i));
        course.getHoleLayout()
                .get(i)
                .getHoles()
                .forEach(hole -> hole.setHoleLayout(course.getHoleLayout().get(i)));
    }
    
    COURSE_REPOSITORY.save(course);
    HOLE_LAYOUT_REPOSITORY.saveAll(course.getHoleLayout());
    return course;
}

CourseController代码

public ResponseEntity<CourseDto> addNewCourse(@RequestBody @NotNull CourseDto courseDto) {
    Course courseRequest = modelMapper.map(courseDto, Course.class);

    if (COURSE_SERVICE.isValid(courseDto.getCourseName()))
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

    Course course = COURSE_SERVICE.createCourse(courseRequest);
    CourseDto courseResponse = modelMapper.map(course, CourseDto.class);
    return new ResponseEntity<>(courseResponse, HttpStatus.CREATED);
}

请求JSON示例

{
  "courseName": "测试球场",
  "holeLayout": [
    {
      "layoutType": 0,
      "front9Yards": 3200,
      "back9Yards": 3300,
      "overallPar": 72,
      "courseRating": 71.2,
      "slopeRating": 125,
      "holes": [
        {
          "holeNumber": 1,
          "yards": 350,
          "par": 4
        },
        {
          "holeNumber": 2,
          "yards": 180,
          "par": 3
        }
      ]
    }
  ]
}

解决方案

1. 修复空指针异常

空指针根源是modelMapper映射时未初始化holeLayout集合,可通过两种方式解决:

  • 在Course实体的无参构造方法中初始化集合:
public Course() {
    this.holeLayout = new ArrayList<>();
}
  • 或者在Controller映射后检查并初始化:
Course courseRequest = modelMapper.map(courseDto, Course.class);
if (courseRequest.getHoleLayout() == null) {
    courseRequest.setHoleLayout(new ArrayList<>());
}

2. 修正关联关系维护逻辑

当前Service存在逻辑冗余与顺序错误,利用级联配置简化操作:

@Override
public Course createCourse(Course course) {
    // 依次维护多级关联关系
    course.getHoleLayout().forEach(layout -> {
        layout.setCourse(course);
        layout.getHoles().forEach(hole -> hole.setHoleLayout(layout));
    });
    
    // 仅保存Course,级联操作会自动保存HoleLayout和Hole
    return COURSE_REPOSITORY.save(course);
}

3. 校验DTO映射配置

确保CourseDto、HoleLayoutDto、HoleDto与对应实体的集合字段名称一致,或在modelMapper中显式配置集合映射规则,避免映射时集合丢失。

4. 确认数据库外键配置

检查数据库中hole_layout表的course_id列、hole表的hole_layout_id列是否存在,且允许为空(或在保存时能被正确赋值)。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 12:36:23