JPA:DELETE WHERE不会删除子项并抛出异常

31

我正在尝试通过JPQL查询从MOTHER中删除大量行。

Mother类定义如下:

@Entity
@Table(name = "MOTHER")
public class Mother implements Serializable {

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "mother", 
               orphanRemoval = true)
    private List<Child> children;    
}

@Entity
@Table(name = "CHILD")
public class Child  implements Serializable {

    @ManyToOne
    @JoinColumn(name = "MOTHER_ID")
    private Mother mother;    
}

正如你所看到的,Mother类有“children”,当执行以下查询时:

String deleteQuery = "DELETE FROM MOTHER WHERE some_condition";
entityManager.createQuery(deleteQuery).executeUpdate();

会抛出异常:

ERROR - ORA-02292: integrity constraint <constraint name> violated - 
                   child record found
当然,我可以先选择要删除的所有对象并将其检索到列表中,然后在迭代它以删除所有检索到的对象,但这种解决方案的性能只会非常糟糕!那么,有没有一种方法可以利用先前的映射有效地删除所有与之关联的 Mother 对象和 Child 对象,而无需首先编写所有子查询的查询呢?
5个回答

41

在JPQL查询中,DELETE(和INSERT)不会通过关系进行级联。这在规范中已经明确说明:

删除操作仅适用于指定类及其子类的实体。它不会级联到相关的实体。

幸运的是,当有级联属性定义时,通过实体管理器执行持久化和删除操作可以实现级联操作。

你可以做以下操作:

  • 获取应该被删除的所有Mother实体实例。
  • 对于每个实例,调用EntityManager.remove()方法。

代码示例如下:

String selectQuery = "SELECT m FROM Mother m WHERE some_condition";  
List<Mother> mothersToRemove = entityManager
    .createQuery(selectQuery)
    .getResultStream()
    .forEach(em::remove);

11
可悲的是,这样做会破坏批量删除所带来的性能优势。为了在非玩具情况下实现此解决方案,您需要将selectQuery包装成分页架构,并清除每个页面的entermanager,以避免在处理大量“Mothers”时耗尽内存。 - NBW
5
每次看到循环中的数据库调用,我都会感到不安。这样做非常慢、占用资源多且效率低下!总有更好的方法! :) - PAULUS
7
通常情况下,EntityManager.remove() 方法并不会立刻操作数据库,而是在稍后的 flush/commit 时才执行。 - Markus Yrjölä
这是真的。JPA实现将在内存中操作,直到需要刷新。这是使用ORM的一个_潜在_好处之一。 - DavidS

2

您尝试过使用session.delete()或等效的EntityManager.remove()吗?

当您使用HQL删除语句发出查询时,可能会绕过Hibernate的级联机制。请看这个JIRA问题:HHH-368

您可能可以通过以下方法实现:

Mother mother = session.load(Mother.class, id);
// If it is a lazy association, 
//it might be necessary to load it in order to cascade properly
mother.getChildren(); 
session.delete(mother);

我现在不确定是否需要初始化集合才能使级联正常。


1
你可以依靠RDBMS使用外键约束来删除那些母亲。这假设你从实体生成你的DDL:
@Entity
@Table(name = "CHILD")
public class Child  implements Serializable {

    @ManyToOne
    @JoinColumn(name = "MOTHER_ID", foreignKey = @ForeignKey(foreignKeyDefinition =
        "FOREIGN KEY(MOTHER_ID) REFERENCES MOTHER(ID) ON DELETE CASCADE",
        value = ConstraintMode.CONSTRAINT))
    private Mother mother;    
}

我刚看到这个答案,想知道如果entityManager/Cache持有子实体的引用会发生什么?我们是否需要设置orphanRemoval? - madz
对我来说,Thorben Janssen关于这个话题的回答很有趣: “不,不建议使用这种方法。 Hibernate不知道数据库上级和下级缓存中的级联操作删除了哪些记录。它无法删除任何映射到已删除记录之一的实体。因此,你面临着缓存包含过时信息(不存在的记录)的高风险。”。请参阅https://thorben-janssen.com/avoid-cascadetype-delete-many-assocations/#comment-34893 - undefined

0
我必须说,我不确定在查询中使用“delete”是否会删除您的所有相关的onetomany实体,就像“MikKo Maunu”所说的那样。我认为它会。 问题是(很抱歉没有尝试过),JPA / Hibernate将执行“真正的SQL删除”,而这些Mother和Child实例此时未被管理,因此无法知道要删除哪些Child实例。 orphanRemoval非常有帮助,但在这种情况下不适用。 我会:
1)尝试将“fetch = FetchType.EAGER”添加到onetomany关系中(这可能也是性能问题)
2)如果1)不起作用,则不要获取所有Mother / Child以使JPA层清晰明了,并在使用之前运行一个查询(在同一事务中,但我不确定您是否需要在它们之间运行'em.flush')
DELETE FROM Child c WHERE c.mother <the condition>

在 JPA/Hibernate 中,删除通常是一个麻烦事,我用一个例子来谴责 ORM 的使用,它本质上是应用程序中的一个附加层,以使事情变得“更容易”。唯一好的事情是,在开发阶段通常会发现 ORM 问题/错误。我的钱总是放在 MyBatis 上,我认为它更干净。

更新:

Mikko Maunu 是对的,JPQL 中的批量删除不会级联。使用我建议的两个查询是可以的。

棘手的问题是,持久性上下文(由 EntityManager 管理的所有实体)与批量删除所做的内容不同步,因此(在我建议的情况下)它们应该在单独的事务中运行。

更新2: 如果使用手动删除而不是批量删除,则许多 JPA 提供程序和 Hibernate 也提供 removeAll(...) 方法或类似的东西(非 API)在其 EntityManager 实现上。它更简单易用,可能在性能方面更有效。

例如,在 OpenJPA 中,您只需要将 EM 强制转换为 OpenJPAEntityManager,最好通过 OpenJPAPersistence.cast(em).removeAll(...)。


你不需要试一下,JPQL中的DELETE操作不会级联。在规范中明确说明:“删除操作仅适用于指定类及其子类的实体。它不会级联到相关的实体。” - Mikko Maunu

0

如果您正在使用Hibernate,这与之相关并且可能提供解决方案。

JPA CascadeType.ALL does not delete orphans

编辑

由于Oracle给出了错误信息,您可以尝试使用Oracle级联删除来解决此问题。但是,这可能会产生不可预测的结果:因为JPA没有意识到您正在删除其他记录,这些对象可能仍然存在于缓存中,并且即使它们已经被删除也可能被使用。只有在您使用的JPA实现具有缓存且已配置为使用它时才适用。

以下是有关在Oracle中使用级联删除的信息:http://www.techonthenet.com/oracle/foreign_keys/foreign_delete.php


Hibernate的deleteOrphans注解对此无济于事。我已经尝试过了。 - DoubleMalt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接