JPA中的orphanRemoval=true与ON DELETE CASCADE DML子句有何不同?

233
我有点困惑于JPA 2.0中的orphanRemoval属性。我认为当使用JPA提供程序的DB生成工具创建基础数据库DDL时需要它,以在特定关系上具有ON DELETE CASCADE。但是,如果该数据库已经存在并且该关系上已经具有ON DELETE CASCADE,那么这不足以适当地级联删除吗?orphanRemoval除了以上操作之外还有什么作用呢?
8个回答

371

orphanRemovalON DELETE CASCADE没有任何关系。

orphanRemoval是完全与ORM有关的事情。它标记“子”实体在不再从“父”实体引用时将被删除,例如当您从父实体的相应集合中删除子实体时。

ON DELETE CASCADE与数据库有关的事情,它在删除“父”行时从数据库中删除“子”行。


4
这是否意味着它们具有相同的效果,但是实现这一效果的系统不同? - Anonymoose
135
Anon,它没有相同的效果。ON DELETE CASCADE告诉数据库在删除父记录时删除所有子记录。也就是说,如果我删除了发票,那么会删除该发票上的所有商品项。OrphanRemoval告诉ORM,如果我从属于发票对象的商品项集合中删除一个商品项对象(在内存中进行操作),然后“保存”发票,则应从底层数据库中删除已删除的商品项。 - garyKeorkunian
5
如果你使用单向关系,即使你没有设置orphanRemoval=true,孤儿也会被自动删除。 - Tim

133
这里举个例子:
当一个Employee实体对象被删除时,删除操作会级联到引用的Address实体对象。在这方面,orphanRemoval=truecascade=CascadeType.REMOVE是相同的,如果指定了orphanRemoval=true,那么CascadeType.REMOVE是多余的。
两种设置之间的区别在于对于断开关系的响应。例如,当将地址字段设置为null或另一个Address对象时。
  • 如果指定了orphanRemoval=true,则断开的Address实例会自动删除。这对于清理应该没有从所有者对象(例如Employee)的引用中存在的依赖对象(例如Address)非常有用。

  • 如果仅指定了cascade=CascadeType.REMOVE,则不会自动执行任何操作,因为断开关系不是一项删除操作。

为避免孤立的引用导致脱离,此功能只应针对持有私有非共享依赖对象的字段启用。
希望这样更清晰明了。

阅读了您的回答后,我意识到它们之间的确切区别,并且我的问题已得到解决。如果从父实体中定义的集合中断开(删除)子实体,则我陷入了从数据库中删除子实体的困境。为此,我提出了这个问题'http://stackoverflow.com/questions/15526440/jpa2-0-hibernate-removing-child-table-rows-with-parent-entity-merge-on-remov'。只是在链接两个问题时添加了我的评论。 - Narendra Verma
@forhas 请查看以下问题:https://stackoverflow.com/questions/58185249/orphanremoval-in-hibernate-not-working-when-multilevel-joining-relations - user9845730

52

当你从集合中移除一个子实体时,你也会将该子实体从数据库中删除。orphanRemoval还意味着你不能更改父级对象;如果有一个部门拥有员工,一旦你将该员工从一个部门中移除并放入另一个部门,那么在flush/commit(以先到者为准)时,你将无意中将该员工从数据库中删除。教训就是只要确定该父节点的子项不会在其存在期间迁移到其他父节点,就将orphanRemoval设置为true。打开orphanRemoval还会自动将REMOVE添加到级联列表中。


3
完全正确...也称为“私人”父/子关系。 - Dave
2
这意味着,一旦我调用 department.remove(emp);,该员工将从 emp 表中删除,甚至不需要调用 commit() - JavaTechnical

44

实体状态转换

JPA将实体状态转换为SQL语句,例如INSERT、UPDATE或DELETE。

JPA entity state transitions

当您使用persist持久化实体时,您正在安排INSERT语句在EntityManager被自动或手动刷新时执行。

当您remove一个实体时,您正在安排DELETE语句,在持久化上下文被刷新时执行。

级联实体状态转换

为了方便,JPA允许您将父实体的状态转换传播到子实体中。

因此,如果您有一个父Post实体,它与PostComment子实体具有@OneToMany关联:

Post and PostComment entities

Post实体中的comments集合映射如下:

@OneToMany(
    mappedBy = "post", 
    cascade = CascadeType.ALL,
    orphanRemoval = true
)
private List<Comment> comments = new ArrayList<>();

CascadeType.ALL

cascade属性告诉JPA提供程序将实体状态转换从父Post实体传递到包含在comments集合中的所有PostComment实体。

因此,如果删除Post实体:

Post post = entityManager.find(Post.class, 1L);
assertEquals(2, post.getComments().size());

entityManager.remove(post);

JPA提供者将首先删除PostComment实体,当所有子实体都被删除后,它将删除Post实体:

DELETE FROM post_comment WHERE id = 1
DELETE FROM post_comment WHERE id = 2

DELETE FROM post WHERE id = 1

孤儿删除

当将orphanRemoval属性设置为true时,JPA提供程序将在从集合中删除子实体时安排一个remove操作。

所以,在我们的案例中,

Post post = entityManager.find(Post.class, 1L);
assertEquals(2, post.getComments().size());

PostComment postComment = post.getComments().get(0);
assertEquals(1L, postComment.getId());

post.getComments().remove(postComment);

JPA提供者将删除相关的post_comment记录,因为PostComment实体不再在comments集合中引用:

DELETE FROM post_comment WHERE id = 1

ON DELETE CASCADE

ON DELETE CASCADE在外键级别上定义:

ALTER TABLE post_comment 
ADD CONSTRAINT fk_post_comment_post_id 
FOREIGN KEY (post_id) REFERENCES post 
ON DELETE CASCADE;

完成此操作后,如果您删除一行post

DELETE FROM post WHERE id = 1

数据库引擎会自动删除所有相关的post_comment实体。但是,如果您错误地删除了根实体,则这可能是一种非常危险的操作。

结论

JPA cascadeorphanRemoval选项的优点在于,您还可以从乐观锁定中受益,以防止丢失更新。

如果使用JPA级联机制,则无需使用DDL级别的ON DELETE CASCADE,否则如果您删除具有多个层级的多个子实体的根实体,则可能会导致非常危险的操作。


在您的答案中的孤儿删除部分:post.getComments().remove(postComment); 只有在一对多双向映射中才能工作,因为它具有持久级联。如果没有级联并且在ManyToOne端没有删除,就像您的示例中一样,两个实体之间的连接删除将不会在数据库中持久化? - aurelije
孤儿删除不受CascadeType影响,它是一种补充机制。现在,您将删除与持久化混淆了。孤儿删除是指删除未引用的关联,而持久化是关于保存新实体。您需要按照答案中提供的链接来更好地理解这些概念。 - Vlad Mihalcea
1
我不明白一件事:如果我们从未在M端删除连接,那么在双向映射中孤儿删除将如何启动?我认为,在不将PostComment.post设置为null的情况下从Post的列表中删除PostComment将不会导致这两个实体之间的连接在数据库中被删除。这就是为什么我认为孤儿删除不会在关系世界中发挥作用的原因,因为PostComment在那里并不是孤儿。我会在有空的时候进行测试。 - aurelije
2
我在我的高性能Java持久化GitHub存储库中添加了这两个示例,演示了它的工作原理。您不需要像直接删除实体那样同步子级。但是,孤儿删除仅在添加级联时才起作用,但这似乎是Hibernate的限制,而不是JPA规范。 - Vlad Mihalcea

21

DDL中ON DELETE CASCADE的等效JPA映射是cascade=CascadeType.REMOVE。孤儿删除指当与其“父”实体的关系被销毁时,依赖实体也会被删除。例如,如果一个子对象从@OneToMany关系中移除但在实体管理器中没有明确移除它。


1
cascade=CascadeType.REMOVE 不等同于 ON DELETE CASCADE。在应用程序代码中执行删除操作不会影响 DDL,而其他操作则在数据库中执行。请参见 https://dev59.com/rWUp5IYBdhLWcg3w_rrT#19696859。 - Grigory Kislin

16

区别在于:
- orphanRemoval = true:当“Child”实体不再被引用时(其父实体可能未被删除),将删除该实体。
- CascadeType.REMOVE:只有在“Parent”被删除时才会删除“Child”实体。


6

@GaryK的回答非常好,我花了一个小时寻找关于orphanRemoval=trueCascadeType.REMOVE之间的区别,他帮助我理解了这个问题。

总结一下:orphanRemoval=trueCascadeType.REMOVE相同,只有当我们删除对象(entityManager.delete(object))并且希望同时删除子对象时才起作用。

在完全不同的情况下,当我们获取一些数据,例如List<Child> childs = object.getChilds(),然后删除一个子项(entityManager.remove(childs.get(0))),使用orphanRemoval=true将导致与childs.get(0)对应的实体记录从数据库中被删除。


1
你的第二段有一个拼写错误:没有entityManager.delete(obj)这个方法,应该是entityManager.remove(obj)。 - JL_SO

3
孤儿清除在以下场景中具有与ON DELETE CASCADE相同的效果: 假设我们在学生实体和导师实体之间有一个简单的多对一关系,其中许多学生可以映射到同一个导师,并且在数据库中,我们有一个外键关系Student和Guide表之间,使得学生表具有id_guide作为FK。
    @Entity
    @Table(name = "student", catalog = "helloworld")
    public class Student implements java.io.Serializable {
     @Id
     @GeneratedValue(strategy = IDENTITY)
     @Column(name = "id")
     private Integer id;

    @ManyToOne(cascade={CascadeType.PERSIST,CascadeType.REMOVE})
    @JoinColumn(name = "id_guide")
    private Guide guide;

// 父实体

    @Entity
    @Table(name = "guide", catalog = "helloworld")
    public class Guide implements java.io.Serializable {

/**
 * 
 */
private static final long serialVersionUID = 9017118664546491038L;

@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id", unique = true, nullable = false)
private Integer id;

@Column(name = "name", length = 45)
private String name;

@Column(name = "salary", length = 45)
private String salary;


 @OneToMany(mappedBy = "guide", orphanRemoval=true) 
 private Set<Student> students = new  HashSet<Student>(0);

在这种情况下,学生实体是关系的所有者,因此我们需要保存学生实体以持久化整个对象图,例如:
    Guide guide = new Guide("John", "$1500");
    Student s1 = new Student(guide, "Roy","ECE");
    Student s2 = new Student(guide, "Nick", "ECE");
    em.persist(s1);
    em.persist(s2);

在这里,我们使用两个不同的学生对象映射相同的指南,并且由于使用了CASCADE.PERSIST,对象图将保存如下所示的数据库表(在我的情况下为MySql)

学生表:

ID 名称 部门 Id_Guide

1     Roy     ECE    1

2    Nick    ECE    1

指南表:

ID 名称 工资

1    John     $1500

现在,如果我想删除其中一个学生,可以使用

      Student student1 = em.find(Student.class,1);
      em.remove(student1);

当学生记录被删除时,相应的指导记录也应该被删除,这时候在学生实体中使用CASCADE.REMOVE属性就很有用了。它的作用是删除标识为1的学生以及相应的指导对象(标识为1)。但在本例中,还有另一个学生对象映射到同一指导记录,如果不在指导实体中使用orphanRemoval=true属性,则上述删除代码将无法起作用。


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