Spring Boot 删除违反非空约束 SQL 错误解决
2025-03-17 01:51:05
Spring Boot 删除记录时违反非空约束的 SQL 错误
在使用 Spring Boot 开发时,经常会遇到数据库操作相关的问题。最近就碰到了一个:在删除具有非空约束的记录时,出现了 SQL 错误。 具体的报错信息是 org.postgresql.util.PSQLException: ERROR: null value in column "fk_emitters_plant_id" of relation "emitters" violates not-null constraint
。 这篇文章就来聊聊这个问题是怎么产生的,以及该怎么解决。
问题
我定义了两个实体类:PlantEntity
和 PlantEmitterEntity
。 PlantEntity
代表植物信息,PlantEmitterEntity
代表植物的排放物信息。一个植物可以有多个排放物信息,所以 PlantEntity
和 PlantEmitterEntity
之间是 一对多 的关系。
当我试图通过 UUID 删除 PlantEntity
的记录时,就报了上面的那个错。
问题原因分析
报错信息很明确:在 "emitters" 表的 "fk_emitters_plant_id" 列中,出现了 null 值,违反了非空约束。
结合代码来看,PlantEmitterEntity
中有一个 plant
字段,它通过 @ManyToOne
注解与 PlantEntity
关联,并且设置了外键 fk_emitters_plant_id
,这个外键是不允许为空的 ( nullable = false
)。
// PlantEmitterEntity.java
@ManyToOne(
fetch = FetchType.LAZY,
cascade = CascadeType.ALL
)
@JoinColumn(
name = "fk_emitters_plant_id",
nullable = false,
updatable = true,
insertable = true,
foreignKey = @ForeignKey(name = "FK_emitters_plant"))
private PlantEntity plant;
当我直接删除 PlantEntity
时,对应的 PlantEmitterEntity
记录的外键 fk_emitters_plant_id
就会变成 null, 这样就违反了非空约束,数据库报错了。 简单地说,就是:我想直接把植物删了,但是它还有相关的排放物信息,数据库不让。
解决方案
要解决这个问题,核心就是要处理好 PlantEntity
和 PlantEmitterEntity
之间的关系。不能直接把 PlantEntity
删除,得先把跟它相关的 PlantEmitterEntity
记录处理掉。 总结下来,大概有这么几种方法:
1. 手动删除关联的 PlantEmitterEntity
记录
这是最直接的方法。 在删除 PlantEntity
之前,先根据 PlantEntity
的 ID,查询出所有关联的 PlantEmitterEntity
记录,然后把这些记录都删掉,最后再删除 PlantEntity
。
- 原理: 清理干净所有依赖,确保不会留下孤立的关联数据。
- 代码示例:
// Service
public void delete(ResponsePlantDTO plant){
PlantEntity plantEntity = mapper.map(plant, PlantEntity.class);
// 1. 先找出所有相关的 PlantEmitterEntity
Set<PlantEmitterEntity> emitters = plantEntity.getEmitter();
// 2. 删除这些 PlantEmitterEntity
if(emitters!=null && !emitters.isEmpty()){
emitterRepository.deleteAll(emitters);
}
//3. 最后删除 PlantEntity
repository.delete(plantEntity);
}
需要在Service中添加 `emitterRepository`:
```java
@Autowired
private PlantEmitterRepository emitterRepository;
```
- 额外说明:
- 需要在Spring boot项目中添加
PlantEmitterRepository
- 这种方式需要手动编写额外的删除逻辑, 但好在控制粒度比较细。
- 需要在Spring boot项目中添加
2. 使用 JPA 的级联删除 (CascadeType.REMOVE 或 CascadeType.ALL)
JPA 提供了级联操作的功能,可以自动处理关联实体之间的操作。 把 PlantEntity
中的 @OneToMany
注解的 cascade
属性设置为 CascadeType.REMOVE
或者 CascadeType.ALL
,就可以实现级联删除。
- 原理:
CascadeType.REMOVE
表示当删除PlantEntity
时,会同时删除关联的PlantEmitterEntity
记录。CascadeType.ALL
包含了CascadeType.REMOVE
,还包括其他级联操作(如 PERSIST, MERGE, REFRESH 等)。 - 代码示例:
// PlantEntity.java
@OneToMany(
fetch = FetchType.EAGER,
cascade = CascadeType.REMOVE, // 或者 CascadeType.ALL
mappedBy = "plant"
)
private Set<PlantEmitterEntity> emitter;
修改PlantEmitterEntity
中的外键关联,修改cascade
为CascadeType.PERSIST
@ManyToOne(
fetch = FetchType.LAZY,
cascade = CascadeType.PERSIST
)
修改Service中delete
方法:
// Service
public void delete(ResponsePlantDTO plant){
repository.deleteById(plant.getId());
}
- 代码解释: 只需要简单地修改
PlantEntity
的注解,代码改动非常少。另外在service中的方法需要进行一定的修改。 - 需要注意的一点是: 使用CascadeType.REMOVE/ALL会删除掉相关联数据,确定一下表中的外键约束是否也设置为ON DELETE CASCADE。
3. 修改数据库外键约束
我们还可以在数据库层面修改外键的约束。 将外键的 ON DELETE
属性设置为 CASCADE
。 这样,当删除 PlantEntity
记录时,数据库会自动删除关联的 PlantEmitterEntity
记录。
-
原理: 通过数据库的外键约束来自动处理关联数据的删除操作。
-
操作步骤:
- 连接到 PostgreSQL 数据库。
- 找到
emitters
表的fk_emitters_plant_id
外键。 - 修改外键定义,添加
ON DELETE CASCADE
。
-
SQL 命令示例:
-- 禁用外键约束
ALTER TABLE emitters DROP CONSTRAINT "FK_emitters_plant";
-- 重新添加外键约束,并设置 ON DELETE CASCADE
ALTER TABLE emitters
ADD CONSTRAINT "FK_emitters_plant"
FOREIGN KEY (fk_emitters_plant_id) REFERENCES plants_voc(id)
ON DELETE CASCADE;
- 额外建议: 直接修改数据库外键约束可能会影响到其他依赖这个外键的程序, 操作前最好进行备份。
4. 使用 @PreRemove 注解(进阶技巧)
在 JPA 中,还可以使用 @PreRemove
注解来定义一个回调方法,这个方法会在实体被删除之前执行。可以在这个方法中手动处理关联关系。
- 原理:
@PreRemove
提供了一个钩子,可以在实体删除前执行自定义的逻辑。 - 代码示例:
// PlantEntity.java
@PreRemove
private void preRemove() {
if (emitter != null) {
emitter.forEach(emitterItem -> emitterItem.setPlant(null));
}
}
修改PlantEmitterEntity
中的外键关联为可以设置空值:
@ManyToOne(
fetch = FetchType.LAZY,
cascade = CascadeType.PERSIST
)
@JoinColumn(
name = "fk_emitters_plant_id",
nullable = true,
updatable = true,
insertable = true,
foreignKey = @ForeignKey(name = "FK_emitters_plant"))
private PlantEntity plant;
- 代码解释: 在
preRemove
方法中,将关联的PlantEmitterEntity
的plant
字段设置为null
,这样就解除了它们之间的关联。注意需要设置PlantEmitterEntity
中的外键列为可以接受null。 - 优势: 代码逻辑可以内聚到实体类内部。但也有坏处,这要求我们将数据库中外键nullable改为true。
总结
删除 Spring Boot 中具有非空约束的记录时,出现 SQL 错误,通常是因为关联关系没有处理好。 解决这个问题的方法有好几种,可以根据实际情况选择最适合的一种。
处理这类问题,最主要的还是仔细分析实体之间的关系,理清关联关系,然后再选择合适的方式去处理。