1. 概述

作为功能完备的ORM框架,Hibernate负责持久化对象(实体)的生命周期管理,包括CRUD操作:读取、保存、更新和删除。

本文将深入探讨使用Hibernate从数据库删除对象的多种方式,并解释常见问题和潜在陷阱。我们主要使用JPA规范,仅在JPA未标准化的特性时回退到Hibernate原生API。

2. 删除对象的不同方式

对象删除场景包括:

  • 使用 EntityManager.remove()
  • 通过其他实体实例级联删除
  • 应用 orphanRemoval 机制
  • 执行 delete JPQL语句
  • 执行原生SQL查询
  • 实现软删除技术(通过 @Where 注解过滤软删除实体)

下文将详细分析这些场景。

3. 使用EntityManager删除

EntityManager 提供最直接的实体删除方式:

Foo foo = new Foo("foo");
entityManager.persist(foo);
flushAndClear();

foo = entityManager.find(Foo.class, foo.getId());
assertThat(foo, notNullValue());
entityManager.remove(foo);
flushAndClear();

assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());

示例中使用的辅助方法用于刷新和清除持久化上下文:

void flushAndClear() {
    entityManager.flush();
    entityManager.clear();
}

调用 EntityManager.remove() 后,实例进入 removed 状态,数据库删除操作将在下次刷新时执行。

⚠️ 关键陷阱:若对已删除实例应用 PERSIST 操作,该实例会被重新持久化。常见错误是忽略级联操作(通常在刷新时由其他实例触发)导致的重新持久化,因为JPA规范3.2.2节强制要求此行为。

通过 FooBar@ManyToOne 关联演示此问题:

@Entity
public class Foo {
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Bar bar;

    // 其他映射、getter和setter
}

当删除被持久化上下文中 Foo 实例引用的 Bar 实例时,删除操作会失败:

Bar bar = new Bar("bar");
Foo foo = new Foo("foo");
foo.setBar(bar);
entityManager.persist(foo);
flushAndClear();

foo = entityManager.find(Foo.class, foo.getId());
bar = entityManager.find(Bar.class, bar.getId());
entityManager.remove(bar);
flushAndClear();

bar = entityManager.find(Bar.class, bar.getId());
assertThat(bar, notNullValue());

foo = entityManager.find(Foo.class, foo.getId());
foo.setBar(null);
entityManager.remove(bar);
flushAndClear();

assertThat(entityManager.find(Bar.class, bar.getId()), nullValue());

若被删除的 BarFoo 引用,PERSIST 操作会从 Foo 级联到 Bar(因关联标记了 cascade = CascadeType.ALL),导致删除被取消。开启 org.hibernate 包的TRACE日志可观察到类似 un-scheduling entity deletion 的记录。

4. 级联删除

父实体删除时,删除操作可级联到子实体:

Bar bar = new Bar("bar");
Foo foo = new Foo("foo");
foo.setBar(bar);
entityManager.persist(foo);
flushAndClear();

foo = entityManager.find(Foo.class, foo.getId());
entityManager.remove(foo);
flushAndClear();

assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());
assertThat(entityManager.find(Bar.class, bar.getId()), nullValue());

bar 被删除是因为关联声明了级联所有生命周期操作。

严重警告:在 @ManyToMany 关联中级联 REMOVE 操作几乎总是错误!这会删除可能被其他父实例引用的子实体。此警告同样适用于 CascadeType.ALL(因其包含 REMOVE)。

5. 孤儿移除

orphanRemoval 指令声明:当关联实体从父实体解除关联(或父实体被删除)时,这些关联实体将被删除。

通过 BarBaz 的关联演示:

@Entity
public class Bar {
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Baz> bazList = new ArrayList<>();

    // 其他映射、getter和setter
}

Baz 实例从父 Bar 的列表中移除时,会被自动删除:

Bar bar = new Bar("bar");
Baz baz = new Baz("baz");
bar.getBazList().add(baz);
entityManager.persist(bar);
flushAndClear();

bar = entityManager.find(Bar.class, bar.getId());
baz = bar.getBazList().get(0);
bar.getBazList().remove(baz);
flushAndClear();

assertThat(entityManager.find(Baz.class, baz.getId()), nullValue());

核心机制orphanRemoval 的语义等同于直接对受影响的子实例应用 REMOVE 操作,且 REMOVE 操作会进一步级联到嵌套子实体。因此必须确保没有其他实例引用被删除的实体(否则它们会被重新持久化)。

6. 使用JPQL语句删除

Hibernate支持DML风格的删除操作:

Foo foo = new Foo("foo");
entityManager.persist(foo);
flushAndClear();

entityManager.createQuery("delete from Foo where id = :id")
  .setParameter("id", foo.getId())
  .executeUpdate();

assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());

⚠️ 重要提醒:DML风格的JPQL语句不会影响已加载到持久化上下文中的实体实例状态或生命周期。建议在加载受影响实体之前执行此类语句。

7. 使用原生查询删除

当需要Hibernate不支持或数据库特定的功能时,可回退到原生查询:

Foo foo = new Foo("foo");
entityManager.persist(foo);
flushAndClear();

entityManager.createNativeQuery("delete from FOO where ID = :id")
  .setParameter("id", foo.getId())
  .executeUpdate();

assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());

⚠️ 同样适用:原生查询不会影响查询执行前已加载到持久化上下文中的实体实例状态或生命周期。

8. 软删除

出于审计和历史记录目的,通常不希望从数据库物理删除数据。此时可采用软删除技术:仅标记记录为已删除,并在查询时过滤掉这些记录。

为避免在所有查询中重复添加过滤条件,Hibernate提供了 @Where 注解。该注解放在实体类上,包含的SQL片段会自动添加到该实体的SQL查询中。

通过在 Foo 实体添加 @Where 注解和 DELETED 列演示:

@Entity
@Where(clause = "DELETED = 0")
public class Foo {
    // 其他映射

    @Column(name = "DELETED")
    private Integer deleted = 0;
    
    // getter和setter

    public void setDeleted() {
        this.deleted = 1;
    }
}

测试验证软删除效果:

Foo foo = new Foo("foo");
entityManager.persist(foo);
flushAndClear();

foo = entityManager.find(Foo.class, foo.getId());
foo.setDeleted();
flushAndClear();

assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());

9. 总结

本文探讨了Hibernate中删除数据的多种方式,解释了核心概念和最佳实践,并演示了如何轻松实现软删除。

本文实现的完整代码可在GitHub项目获取。这是一个基于Maven的项目,可直接导入运行。


原始标题:Deleting Objects with Hibernate