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
相关产品推荐
相关产品推荐

