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

Hibernate保存主从实体时非空字段约束报错求助

问题

拥有Rule和RuleUnitTest两个实体(一对多关联),创建Rule时会自动生成ruleId,需将该ruleId关联到RuleUnitTest中。相关代码及报错信息如下:

Rule实体

package io.avalor.webserver.rule;

import com.vladmihalcea.hibernate.type.array.EnumArrayType;
import com.vladmihalcea.hibernate.type.array.ListArrayType;
import com.vladmihalcea.hibernate.type.json.JsonBinaryType;
import io.avalor.webserver.account.Account;
import io.avalor.webserver.customquery.CustomQuery;
import io.avalor.webserver.persistency.AbstractEntity;
import io.avalor.webserver.rule.ruletest.RuleTest;
import io.avalor.webserver.rule.unittest.RuleUnitTest;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.*;

import javax.persistence.*;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Set;

@Entity
@Table
@SQLDelete(sql = "UPDATE rule SET deleted = true WHERE rule_id=?")
@Where(clause = "deleted=false")
@TypeDef(name = "list-array", typeClass = EnumArrayType.class, parameters = {@org.hibernate.annotations.Parameter(name = EnumArrayType.SQL_ARRAY_TYPE, value = "varchar")})
@TypeDef(name = "json", typeClass = JsonBinaryType.class)
@TypeDef(name = "list", typeClass = ListArrayType.class)
@Getter
@Setter
public class Rule extends AbstractEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "rule_id")
    private String id;

    @Column
    @NotBlank
    private String name;

    @OneToMany(fetch = FetchType.EAGER)
    @Fetch(value = FetchMode.SUBSELECT)
    @JoinTable(
            name = "rule_unit_test",
            joinColumns = {@JoinColumn(name = "rule_id", updatable = false)},
            inverseJoinColumns = {@JoinColumn(name = "test_id", updatable = false)}
    )
    private List<RuleUnitTest> unitTests;
}

RuleUnitTest实体

package io.avalor.webserver.rule.unittest;

import com.fasterxml.jackson.databind.JsonNode;
import io.avalor.webserver.persistency.AbstractEntity;
import io.avalor.webserver.rule.Rule;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.*;

import javax.persistence.*;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;

@Entity
@Table
@Where(clause = "deleted=false")
@SQLDelete(sql = "UPDATE rule_unit_test SET deleted = true WHERE test_id=?")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class RuleUnitTest {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "test_id")
    private String id;

    @NotNull
    @JoinColumn(name = "rule_id", updatable = false)
    @ManyToOne(fetch = FetchType.LAZY)
    private Rule rule;

    @Column
    @NotBlank
    private String name;

    @NotEmpty
    @Type(type = "json")
    @Column(name = "test_data")
    private List<JsonNode> testData;
}

Rule服务层方法

@Transactional
public RuleDto create(RuleDto ruleDto) {
    Rule rule = ruleMapper.dtoToEntity(ruleDto);
    Rule savedRule = ruleRepository.save(rule);

    savedRule = saveUnitTestsIfExists(savedRule,ruleDto.getUnitTests());
    // ruleRepository.save(savedRule);
    return ruleMapper.entityToDto(savedRule);
}

private Rule saveUnitTestsIfExists(Rule rule, List<RuleUnitTestDto> unitTestsDto) {
    if (CollectionUtils.isNotEmpty(unitTestsDto)) {
        return unitTestService.create(rule,unitTestsDto);
    }
    return rule;
}

RuleUnitTest服务层代码

public Rule create(Rule rule, List<RuleUnitTestDto> unitTestsDto) {
    List<RuleUnitTestDto> limitedRuleUnitTestDtos = unitTestsDto;

    if (unitTestsDto.size() > MAX_TESTS_PER_RULE) {
        limitedRuleUnitTestDtos = unitTestsDto.subList(0, MAX_TESTS_PER_RULE);
    }
    List<RuleUnitTest> unitTests = ruleUnitTestMapper.buildBuildUnitTestFromUnitTest(rule, limitedRuleUnitTestDtos);


    rule.setUnitTests(unitTestRepository.saveAll(unitTests));
    return rule;
}

Mapper代码

package io.avalor.webserver.rule;


import io.avalor.webserver.rule.unittest.RuleUnitTest;
import io.avalor.webserver.rule.unittest.RuleUnitTestDto;
import org.mapstruct.*;

import java.util.*;

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public abstract class RuleMapper {


    @Mapping(source = "account.id", target = "accountId")
    @Mapping(target = "dataSourceTypes", ignore = true)
    public abstract RuleDto entityToDto(Rule rule);

    @Mapping(target = "account.id", source = "accountId")
    @Mapping(target = "dataSourceTypes", ignore = true)
    public abstract Rule dtoToEntity(RuleDto ruleDto);

    @AfterMapping
    public void setRuleProperties(@MappingTarget Rule rule, RuleDto ruleDto) {
        if (rule.getId() != null) {
            rule.setUnitTests(buildBuildUnitTestFromUnitTest(rule, ruleDto.getUnitTests()));
        } else {
            rule.setUnitTests(null);
        }
    }

    @AfterMapping
    public RuleDto enrichDto(@MappingTarget RuleDto ruleDto, Rule rule) {
        if (rule.getUnitTests() != null) {
            ruleDto.setUnitTests(rule.getUnitTests().stream().map(unitTest -> {
                RuleUnitTestDto unitTestDto = new RuleUnitTestDto();
                unitTestDto.setId(unitTest.getId());
                unitTestDto.setName(unitTest.getName());
                unitTestDto.setTestData(unitTest.getTestData());
                unitTestDto.setRuleId(rule.getId());
                return unitTestDto;
            }).toList());
        }
        return ruleDto;
    }

    public List<RuleUnitTest> buildBuildUnitTestFromUnitTest(Rule rule, List<RuleUnitTestDto> unitTestDtos) {
        if (unitTestDtos == null)
            return Collections.emptyList();
        List<RuleUnitTest> unitTestsList = new ArrayList<>();

        for (RuleUnitTestDto unitTestDto : unitTestDtos) {
            unitTestsList.add(new RuleUnitTest(null, rule, unitTestDto.getName(), unitTestDto.getTestData()));
        }
        return unitTestsList;
    }
}

请求体

{
    "name": "rule1",
    "unitTests":[{
        "name":"blah blah",
        "testData":[{"a":"b"}]
    }]
}

报错信息

ERROR: null value in column "name" of relation "rule_unit_test" violates not-null constraint
Detail: Failing row contains (p0n9vr, 07rplw, null, null).

调试时确认name和testData字段并非空值,但仍触发非空约束报错,请问可能的原因是什么?


分析与解决方案

核心原因:双向关联配置冲突导致重复插入

Rule实体中用@JoinTable定义一对多关系,但RuleUnitTest实体本身已经通过@ManyToOne+@JoinColumn(name = "rule_id")维护了外键关联。这种重复配置会让Hibernate执行两次插入:

  1. 第一次调用unitTestRepository.saveAll(unitTests)时,正常插入带name、testData和rule_id的记录;
  2. 事务提交时,Hibernate根据Rule的@JoinTable配置,尝试向rule_unit_test表插入仅含rule_id和test_id的记录,此时name和testData为空,触发非空约束。

解决步骤

  1. 修正Rule实体的关联配置:移除@JoinTable,改用mappedBy指定由RuleUnitTest的rule属性维护关联,避免重复操作:
@OneToMany(fetch = FetchType.EAGER, mappedBy = "rule", cascade = CascadeType.ALL)
@Fetch(value = FetchMode.SUBSELECT)
private List<RuleUnitTest> unitTests;
  • 加上cascade = CascadeType.ALL后,保存Rule时会自动级联保存关联的RuleUnitTest,还能简化代码逻辑。
  1. 验证构造参数顺序:当前RuleUnitTest的全参构造函数参数顺序(id, rule, name, testData)和创建实例时的参数一致,这部分没问题,但后续修改构造函数要注意对应关系。

额外建议

  • 双向关联里别同时用@JoinTable和@JoinColumn,选一方维护关联即可,通常由多的一方(RuleUnitTest)维护外键更符合数据库设计规范;
  • 检查rule_unit_test表的字段定义,确保name和test_data的非空约束和实体类注解一致,避免配置不一致引发问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 20:33:08