Contents

Hibernate 删除数据

1. 概述

作为一个全功能的 ORM 框架,Hibernate 负责持久化对象(实体)的生命周期管理,包括readsaveupdatedelete等 CRUD 操作。

在本文中,我们探讨了**使用 Hibernate 从数据库中删除对象的各种方法,**并解释了可能出现的常见问题和陷阱。

我们使用 JPA,只是退后一步,将 Hibernate 本机 API 用于 JPA 中未标准化的那些特性。

2. 删除对象的不同方式

在以下情况下可能会删除对象:

  • 通过使用EntityManager.remove
  • 当从其他实体实例级联删除时
  • 应用orphanRemoval
  • 通过执行*delete *JPQL 语句
  • 通过执行数据库SQL语句
  • 通过应用软删除技术(通过*@Where*子句中的条件过滤软删除的实体)

在本文的其余部分,我们将详细介绍这些要点。

3. 使用实体管理器删除

使用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方法后,提供的实例将转换为已*delete *状态,并且在下一次刷新时从数据库中删除相关联的内容。

请注意,如果对已删除的实例应用了PERSIST操作,则会重新保留该实例。一个常见的错误是忽略PERSIST操作已应用于已删除的实例(通常是因为它在刷新时从另一个实例级联),因为JPA 规范 的第3.2.2节要求这样的实例是在这种情况下再次坚持。

我们通过定义从FooBar的*@ManyToOne*关联来说明这一点:

@Entity
public class Foo {
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Bar bar;
    // other mappings, getters and setters
}

当我们删除同样加载到持久化上下文中的Foo实例引用的Bar实例时,该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包启用跟踪日志级别并搜索诸如取消调度实体删除之类的条目。

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被删除,因为删除是从foo级联的,因为声明关联将所有生命周期操作从Foo级联到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<>();
    // other mappings, getters and setters
}

然后,当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());

与 JPA DML 样式语句相同的建议适用于本机查询,即本机查询既不影响在执行查询之前加载到持久性上下文中的实体实例的状态也不影响其生命周期

8. 软删除

由于审计目的和保留历史记录,通常不希望从数据库中删除数据。在这种情况下,我们可以应用一种称为软删除的技术。基本上,我们只是将一行标记为已删除,并在检索数据时将其过滤掉。

为了避免在读取软删除实体的所有查询中的where子句中出现大量冗余条件,Hibernate 提供了*@Where*注释,该注释可以放置在实体上,其中包含一个 SQL 片段,该片段会自动添加到生成的 SQL 查询中那个实体。

为了证明这一点,我们将*@Where注释和一个名为DELETED的列添加到Foo*实体:

@Entity
@Where(clause = "DELETED = 0")
public class Foo {
    // other mappings
    @Column(name = "DELETED")
    private Integer deleted = 0;
    
    // getters and setters
    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());