返回

Spring Boot 删除违反非空约束 SQL 错误解决

java

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。 这篇文章就来聊聊这个问题是怎么产生的,以及该怎么解决。

问题

我定义了两个实体类:PlantEntityPlantEmitterEntityPlantEntity 代表植物信息,PlantEmitterEntity 代表植物的排放物信息。一个植物可以有多个排放物信息,所以 PlantEntityPlantEmitterEntity 之间是 一对多 的关系。

当我试图通过 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, 这样就违反了非空约束,数据库报错了。 简单地说,就是:我想直接把植物删了,但是它还有相关的排放物信息,数据库不让。

解决方案

要解决这个问题,核心就是要处理好 PlantEntityPlantEmitterEntity 之间的关系。不能直接把 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
    • 这种方式需要手动编写额外的删除逻辑, 但好在控制粒度比较细。

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 中的外键关联,修改cascadeCascadeType.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 记录。

  • 原理: 通过数据库的外键约束来自动处理关联数据的删除操作。

  • 操作步骤:

    1. 连接到 PostgreSQL 数据库。
    2. 找到 emitters 表的 fk_emitters_plant_id 外键。
    3. 修改外键定义,添加 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 方法中,将关联的 PlantEmitterEntityplant 字段设置为 null,这样就解除了它们之间的关联。注意需要设置PlantEmitterEntity 中的外键列为可以接受null。
  • 优势: 代码逻辑可以内聚到实体类内部。但也有坏处,这要求我们将数据库中外键nullable改为true。

总结

删除 Spring Boot 中具有非空约束的记录时,出现 SQL 错误,通常是因为关联关系没有处理好。 解决这个问题的方法有好几种,可以根据实际情况选择最适合的一种。
处理这类问题,最主要的还是仔细分析实体之间的关系,理清关联关系,然后再选择合适的方式去处理。