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

SpringBoot嵌套保存实体触发PostgreSQL外键约束异常求助

问题

嵌套保存包含子实体的Module时触发PostgreSQL外键约束错误:

org.postgresql.util.PSQLException: ERROR: insert or update on table violates foreign key constraint

单独保存各实体正常,但级联嵌套保存失败。相关实体代码、接口及请求JSON如下:

实体代码

@Entity(name = "question")
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Question extends PersistableObject{
    @Id
    Long id;

    @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.MERGE, CascadeType.PERSIST })
    @JoinColumn(name = "id")
    private List<Answer> answers = new ArrayList<>();

    Date created = new Date();

    private String description;


    @ManyToOne(fetch = FetchType.LAZY, cascade = { CascadeType.MERGE })
    @JoinColumn(name = "id", insertable = false, updatable = false)
    @JsonIgnore
    private Module module;
    //getters and setters
    // constructors
}

@Entity(name = "answer")
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
public class Answer extends PersistableObject {
    @ManyToOne
    @JoinColumn
    Question nextQuestion;

    @ManyToOne
    @JoinColumn(name = "id_question")
    Question prevQuestion;


    private Date created = new Date();

    @Basic
    private String description;

    @ManyToOne(fetch = FetchType.LAZY, cascade = { CascadeType.MERGE })
    @JoinColumn(name = "module", referencedColumnName = "id", insertable = false, updatable = false)
    private Module module;

}


@Entity(name = "module")
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
public class Module extends PersistableObject {

    String description;

    @OneToMany(fetch = FetchType.LAZY,mappedBy = "module",cascade = { CascadeType.MERGE, CascadeType.ALL })
    private List<Answer> answers = new ArrayList<>();

    @OneToMany(fetch = FetchType.LAZY,mappedBy = "module",cascade = { CascadeType.MERGE, CascadeType.ALL })
    private List<Question> questions = new ArrayList<>();

}

@MappedSuperclass
@Getter
@Setter
@EqualsAndHashCode
public abstract class PersistableObject extends RepresentationModel<PersistableObject> {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    Date created = new Date();

}

接口代码

@PostMapping("/")
public ResponseEntity<Module> create(@RequestBody Module module) {

    Module result = moduleService.save(module);

    URI uri = ServletUriComponentsBuilder.fromCurrentRequest()
            .path("/{id}")
            .buildAndExpand(result.getId())
            .toUri();

    return ResponseEntity
                .created(uri)
                .body(result);
}

请求JSON

{
    "id": 1,
    "created": "2022-11-23T08:24:30.878+00:00",
    "description": "MDD",
    "answers": [
        {
            "id": 1,
            "created": "2022-11-23T08:24:30.878+00:00",
            "description": "AN1",
            "moduleId": 1
        },
        {
            "id": 2,
            "created": "2022-11-23T08:24:30.878+00:00",
            "description": "AN2",
            "moduleId": 1
        }
    ],
    "questions": [
        {
            "id": 1,
            "created": "2022-11-23T08:24:30.878+00:00",
            "answers": [
                {
                    "id": 3,
                    "created": "2022-11-23T08:24:30.878+00:00",
                    "description": "AN1",
                    "moduleId": 1
                },
                {
                    "id": 4,
                    "created": "2022-11-23T08:24:30.878+00:00",
                    "description": "AN2",
                    "moduleId": 1
                }
            ],
            "description": "Q1"
        }
    ]
}

解决方案

1. 修复外键映射错误

  • Question与Module的关联:@JoinColumn(name = "id") 错误指向了Question自身的主键,应改为映射到Module的外键列(比如module_id),并移除insertable = false, updatable = false限制:
    @ManyToOne(fetch = FetchType.LAZY, cascade = { CascadeType.MERGE })
    @JoinColumn(name = "module_id")
    @JsonIgnore
    private Module module;
    
  • Answer与Module的关联:insertable = false, updatable = false导致级联保存时无法写入外键值,同时规范列名为module_id:
    @ManyToOne(fetch = FetchType.LAZY, cascade = { CascadeType.MERGE })
    @JoinColumn(name = "module_id", referencedColumnName = "id")
    private Module module;
    
  • Question与Answer的关联:@JoinColumn(name = "id")错误,应指向Answer表中关联Question的外键列(比如question_id):
    @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.MERGE, CascadeType.PERSIST }, orphanRemoval = true)
    @JoinColumn(name = "question_id")
    private List<Answer> answers = new ArrayList<>();
    

2. 维护双向关联引用

级联保存时,必须手动维护双向关联的双方引用,否则Hibernate无法正确写入外键。在moduleService.save方法中添加逻辑:

module.getQuestions().forEach(question -> {
    question.setModule(module);
    // 同时维护Question下Answer的关联
    question.getAnswers().forEach(answer -> {
        answer.setModule(module);
        answer.setPrevQuestion(question);
    });
});
module.getAnswers().forEach(answer -> answer.setModule(module));

3. 修正实体重复字段

Question和Answer类中重复定义了created字段,与父类PersistableObject的created冲突,直接移除子类中的该字段,统一使用父类的属性。

4. 调整请求JSON

  • 移除所有手动指定的id字段:实体主键是自增策略,由数据库自动生成,手动指定会导致主键冲突。
  • 确保moduleId与实体属性映射:在Answer和Question的module属性上添加@JsonProperty("moduleId")注解,让Jackson能正确解析JSON中的字段。

5. 确认级联类型

Module类中对Question和Answer的级联使用CascadeType.ALL已经包含了持久化、更新等操作,无需额外调整,但建议添加orphanRemoval = true确保删除父实体时自动清理子实体。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 16:50:28