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

Spring JPA删除OptionGroup触发DataIntegrityViolationException问题排查

问题:删除OptionGroup时Hibernate尝试将owner_id设为null违反非空约束

删除OptionGroup实体时,Hibernate会执行将其owner_id字段设为null的更新操作,这直接违反了该字段的非空约束。相关日志如下:

2022-08-30 20:17:53.008 DEBUG 17488 --- [nio-8081-exec-1] org.hibernate.SQL                        : update option_group set description=?, option_group_name=?, owner_id=? where id=?
2022-08-30 20:17:53.008 TRACE 17488 --- [nio-8081-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [null]
2022-08-30 20:17:53.008 TRACE 17488 --- [nio-8081-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Männliche Vornamen]
2022-08-30 20:17:53.008 TRACE 17488 --- [nio-8081-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [null]
2022-08-30 20:17:53.008 TRACE 17488 --- [nio-8081-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [20001]
2022-08-30 20:17:53.012  WARN 17488 --- [nio-8081-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 0, SQLState: 23502
2022-08-30 20:17:53.012 ERROR 17488 --- [nio-8081-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper   : ERROR: NULL value in column »owner_id« of relation »option_group« violates Not-Null-Constraint

我并未在OptionGroup的owner字段上配置级联删除,无法理解此行为。若移除owner字段的非空约束,操作会正常执行:OptionGroup被删除,对应的User(owner)保留。


相关实体及代码

OptionGroup实体类

@Entity
@Table(name = "option_group"/*, uniqueConstraints = {
        @UniqueConstraint(columnNames = {  "owner_id", "option_group_name" }) }*/)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class OptionGroup {

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

    @NotBlank(message = "Option Group name is mandatory")
    @Column(name = "option_group_name")
    private String optionGroupName;

    private String description;

    @NotNull(message = "Owner cannot be null")
    @ManyToOne(fetch = FetchType.LAZY, cascade={CascadeType.PERSIST})
    @JoinColumn(name = "ownerId")
    private User owner;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "optionGroup", orphanRemoval = true)
    @NotEmpty(message = "Options cannot be empty")
    private List<Option> options;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "optionGroup", orphanRemoval = true)
    private List<Invitation> invitations;

    @Override
    public int hashCode() {
        return Objects.hash(description, id, optionGroupName,
                options == null ? null : options.stream().map(option -> option.getId()).toList(), owner,
                invitations == null ? null : invitations.stream().map(invitation -> invitation.getId()).toList());
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        OptionGroup other = (OptionGroup) obj;
        return Objects.equals(description, other.description) && Objects.equals(id, other.id)
                && Objects.equals(optionGroupName, other.optionGroupName)
                && Objects.equals(options == null ? null : options.stream().map(option -> option.getId()).toList(),
                        other.options == null ? null : other.options.stream().map(option -> option.getId()).toList())
                && Objects.equals(owner, other.owner)
                && Objects.equals(
                        invitations == null ? null
                                : invitations.stream().map(invitation -> invitation.getId()).toList(),
                        other.invitations == null ? null
                                : other.invitations.stream().map(invitation -> invitation.getId()).toList());
    }

    @Override
    public String toString() {
        return "OptionGroup [id=" + id + ", optionGroupName=" + optionGroupName + ", description=" + description
                + ", owner=" + owner + ", options="
                + (options == null ? null : options.stream().map(option -> option.getId()).toList()) + ", invitations="
                + (invitations == null ? null : invitations.stream().map(invitation -> invitation.getId()).toList())
                + "]";
    }
}

User实体类

@Entity
@Table(name = "usert", uniqueConstraints = {
        @UniqueConstraint(columnNames = { "email"}) })
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Email(message = "Email is not valid", regexp = "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])")
    private String email;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner", orphanRemoval = true)
    private List<OptionGroup> ownedOptionGroups;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "invitee", orphanRemoval = true)
    private List<Invitation> invitations;
}

删除操作代码

OptionGroupService类

@Service
@Transactional
@AllArgsConstructor
public class OptionGroupService {

    @Autowired
    private OptionGroupRepository optionGroupRepository;

    public void deleteOptionGroupById(Long id) throws ObjectWithIdDoesNotExistException {
        if (optionGroupRepository.existsById(id)) {
            optionGroupRepository.deleteById(id);
        } else {
            throw new ObjectWithIdDoesNotExistException("Option Group", id);
        }
    }
}

Repository接口

public interface OptionGroupRepository extends JpaRepository<OptionGroup, Long> {}

原因分析及解决方案

原因

问题根源在User实体的ownedOptionGroups属性配置:

  • cascade = CascadeType.ALL包含了CascadeType.REMOVE,当删除OptionGroup时,Hibernate会尝试同步双向关联的User端集合
  • orphanRemoval = true意味着当OptionGroup从User的集合中被移除时,Hibernate会将其视为"孤儿"进行删除,但在此之前,它会先执行解除关联的操作——将OptionGroup的owner_id设为null,这就触发了非空约束的违反。

解决方案

修改User实体中ownedOptionGroups的配置,移除不必要的级联类型和孤儿移除属性:

@OneToMany(mappedBy = "owner", cascade={CascadeType.PERSIST, CascadeType.MERGE})
private List<OptionGroup> ownedOptionGroups;
  • 仅保留需要的级联类型(如PERSIST和MERGE),避免级联删除
  • 去掉orphanRemoval = true,因为我们是主动删除OptionGroup,而非从User的集合中移除它

另外,建议将OptionGroup中@JoinColumn(name = "ownerId")改为@JoinColumn(name = "owner_id"),与数据库列名保持一致,避免Hibernate自动转换可能带来的潜在问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 15:24:40