JPA和Hibernate级联类型的初学者指南

介绍

JPA实体状态转换转换为数据库DML语句。 由于对实体图进行操作很常见,因此JPA允许我们将实体状态更改从父级传播到子级

通过CascadeType映射配置此行为。

JPA与Hibernate级联类型

Hibernate支持所有JPA级联类型和一些其他旧式级联样式。 下表绘制了JPA级联类型与其等效的Hibernate本机API之间的关联:

JPA EntityManager操作 JPA CascadeType 休眠本机会话操作 休眠原生CascadeType 事件监听器
分离(实体) 分离 逐出(实体) 分离

EVICT
默认驱逐事件侦听器
合并(实体) 合并 合并(实体) 合并 默认合并事件监听器
坚持(实体) 坚持 坚持(实体) 坚持 默认的持久事件监听器
刷新(实体) 刷新 刷新(实体) 刷新 默认刷新事件监听器
删除(实体) 去掉 删除(实体) 删除删除 默认删除事件监听器
saveOrUpdate(实体) SAVE_UPDATE 默认的保存或更新事件监听器
复制(实体,复制模式) 复制 默认复制事件监听器
锁(实体,lockModeType) buildLockRequest(实体,lockOptions) 默认锁定事件监听器
以上所有EntityManager方法 所有 以上所有的Hibernate Session方法 所有

从该表可以得出以下结论:

  • 在JPA EntityManager或Hibernate Session上调用persistmergerefresh没有什么区别。
  • JPA的removedetach调用被委托给Hibernate Delete逐出本机操作。
  • 只有Hibernate支持复制saveOrUpdate 。 尽管复制对于某些非常特定的场景很有用(当确切的实体状态需要在两个不同的数据源之间进行镜像时),但持久 合并合并始终是比本机saveOrUpdate操作更好的替代方法。将持久性用于TRANSIENT实体,将其用于已分离的实体。saveOrUpdate的缺点(将分离的实体快照传递给已经管理该实体的Session时 )导致了合并操作的前身:现已不存在的saveOrUpdateCopy操作。
  • JPA锁定方法与Hibernate锁定请求方法具有相同的行为。
  • JPA CascadeType.ALL不仅适用于EntityManager状态更改操作,而且还适用于所有Hibernate CascadeTypes 。因此,如果将关联与CascadeType.ALL映射,您仍然可以级联Hibernate特定事件。 例如,即使JPA没有定义LOCK CascadeType ,您也可以级联JPA锁定操作(尽管它表现为重新附加,而不是实际的锁定请求传播)。

级联最佳做法

级联仅对父级 - 子级关联有意义( 父级实体状态转换级联到其子级实体)。 从孩子级联到父级不是很有用,通常是映射代码的味道。

接下来,我将采取分析所有JPA 家长的级联行为- 关联。

一对一

最常见的一对一双向关联如下所示:

@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @OneToOne(mappedBy = "post",
        cascade = CascadeType.ALL, orphanRemoval = true)
    private PostDetails details;

    public Long getId() {
        return id;
    }

    public PostDetails getDetails() {
        return details;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void addDetails(PostDetails details) {
        this.details = details;
        details.setPost(this);
    }

    public void removeDetails() {
        if (details != null) {
            details.setPost(null);
        }
        this.details = null;
    }
}

@Entity
public class PostDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(name = "created_on")
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdOn = new Date();

    private boolean visible;

    @OneToOne
    @PrimaryKeyJoinColumn
    private Post post;

    public Long getId() {
        return id;
    }

    public void setVisible(boolean visible) {
        this.visible = visible;
    }

    public void setPost(Post post) {
        this.post = post;
    }
}

Post实体扮演Parent角色,而PostDetailsChild

双向关联应始终在两侧进行更新,因此父级侧应包含addChildremoveChild组合。 这些方法确保我们始终同步关联的双方,以避免对象或关系数据损坏问题。

在这种特定情况下,删除CascadeType.ALL和孤立的孤岛是有意义的,因为PostDetails生命周期与其 实体的生命周期绑定在一起。

进行一对一的持久化操作

CascadeType.PERSISTCascadeType.ALL配置一起提供,因此我们只需要持久化Post实体,并且关联的PostDetails实体也可以持久化:

Post post = new Post();
post.setName("Hibernate Master Class");

PostDetails details = new PostDetails();

post.addDetails(details);

session.persist(post);

生成以下输出:

INSERT INTO post(id, NAME) 
VALUES (DEFAULT, Hibernate Master Class'')

insert into PostDetails (id, created_on, visible) 
values (default, '2015-03-03 10:17:19.14', false)

级联一对一合并操作

CascadeType.MERGE继承自CascadeType.ALL设置,因此我们只需要合并Post实体,并且关联的PostDetails也将合并:

Post post = newPost();
post.setName("Hibernate Master Class Training Material");
post.getDetails().setVisible(true);

doInTransaction(session -> {
    session.merge(post);
});

合并操作生成以下输出:

SELECT onetooneca0_.id     AS id1_3_1_,
   onetooneca0_.NAME       AS name2_3_1_,
   onetooneca1_.id         AS id1_4_0_,
   onetooneca1_.created_on AS created_2_4_0_,
   onetooneca1_.visible    AS visible3_4_0_
FROM   post onetooneca0_
LEFT OUTER JOIN postdetails onetooneca1_ 
    ON onetooneca0_.id = onetooneca1_.id
WHERE  onetooneca0_.id = 1

UPDATE postdetails SET 
    created_on = '2015-03-03 10:20:53.874', visible = true 
WHERE  id = 1

UPDATE post SET 
    NAME = 'Hibernate Master Class Training Material' 
WHERE  id = 1

级联一对一删除操作

CascadeType.REMOVE也是从CascadeType.ALL配置继承的,因此Post实体删除也会触发PostDetails实体删除:

Post post = newPost();

doInTransaction(session -> {
    session.delete(post);
});

生成以下输出:

delete from PostDetails where id = 1
delete from Post where id = 1

一对一删除孤立级联操作

如果一个孩子实体从母公司分离,儿童外键设置为NULL。 如果我们也要删除“ 行”,则必须使用孤立删除支持。

doInTransaction(session -> {
    Post post = (Post) session.get(Post.class, 1L);
    post.removeDetails();
});

除去孤儿将生成以下输出:

SELECT onetooneca0_.id         AS id1_3_0_,
       onetooneca0_.NAME       AS name2_3_0_,
       onetooneca1_.id         AS id1_4_1_,
       onetooneca1_.created_on AS created_2_4_1_,
       onetooneca1_.visible    AS visible3_4_1_
FROM   post onetooneca0_
LEFT OUTER JOIN postdetails onetooneca1_
    ON onetooneca0_.id = onetooneca1_.id
WHERE  onetooneca0_.id = 1

delete from PostDetails where id = 1

单向一对一关联

大多数情况下, 实体是反方(如的mappedBy), 儿童 controling通过它的外键关联。 但是级联不限于双向关联,我们还可以将其用于单向关系:

@Entity
public class Commit {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String comment;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinTable(
        name = "Branch_Merge_Commit",
        joinColumns = @JoinColumn(
            name = "commit_id", 
            referencedColumnName = "id"),
        inverseJoinColumns = @JoinColumn(
            name = "branch_merge_id", 
            referencedColumnName = "id")
    )
    private BranchMerge branchMerge;

    public Commit() {
    }

    public Commit(String comment) {
        this.comment = comment;
    }

    public Long getId() {
        return id;
    }

    public void addBranchMerge(
        String fromBranch, String toBranch) {
        this.branchMerge = new BranchMerge(
             fromBranch, toBranch);
    }

    public void removeBranchMerge() {
        this.branchMerge = null;
    }
}

@Entity
public class BranchMerge {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String fromBranch;

    private String toBranch;

    public BranchMerge() {
    }

    public BranchMerge(
        String fromBranch, String toBranch) {
        this.fromBranch = fromBranch;
        this.toBranch = toBranch;
    }

    public Long getId() {
        return id;
    }
}

层叠在于传播实体状态过渡到一个或多个儿童的实体,它可用于单向和双向关联。

一对多

最常见的 - 关联由一到多和多到一的关系,其中级联是只对一个一对多侧有用:

@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

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

    public void setName(String name) {
        this.name = name;
    }

    public List<Comment> getComments() {
        return comments;
    }

    public void addComment(Comment comment) {
        comments.add(comment);
        comment.setPost(this);
    }

    public void removeComment(Comment comment) {
        comment.setPost(null);
        this.comments.remove(comment);
    }
}

@Entity
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne
    private Post post;

    private String review;

    public void setPost(Post post) {
        this.post = post;
    }

    public String getReview() {
        return review;
    }

    public void setReview(String review) {
        this.review = review;
    }
}

就像一对一的示例一样, CascadeType.ALL和孤立删除是合适的,因为Comment生命周期绑定到其Post Parent实体的生命周期。

级联一对多持久化操作

我们只需要保留Post实体,所有相关的Comment实体也将保留:

Post post = new Post();
post.setName("Hibernate Master Class");

Comment comment1 = new Comment();
comment1.setReview("Good post!");
Comment comment2 = new Comment();
comment2.setReview("Nice post!");

post.addComment(comment1);
post.addComment(comment2);

session.persist(post);

持久操作将生成以下输出:

insert into Post (id, name) 
values (default, 'Hibernate Master Class')

insert into Comment (id, post_id, review) 
values (default, 1, 'Good post!')

insert into Comment (id, post_id, review) 
values (default, 1, 'Nice post!')

级联一对多合并操作

合并Post实体也将合并所有Comment实体:

Post post = newPost();
post.setName("Hibernate Master Class Training Material");

post.getComments()
    .stream()
    .filter(comment -> comment.getReview().toLowerCase()
         .contains("nice"))
    .findAny()
    .ifPresent(comment -> 
        comment.setReview("Keep up the good work!")
);

doInTransaction(session -> {
    session.merge(post);
});

生成以下输出:

SELECT onetomanyc0_.id    AS id1_1_1_,
       onetomanyc0_.NAME  AS name2_1_1_,
       comments1_.post_id AS post_id3_1_3_,
       comments1_.id      AS id1_0_3_,
       comments1_.id      AS id1_0_0_,
       comments1_.post_id AS post_id3_0_0_,
       comments1_.review  AS review2_0_0_
FROM   post onetomanyc0_
LEFT OUTER JOIN comment comments1_
    ON onetomanyc0_.id = comments1_.post_id
WHERE  onetomanyc0_.id = 1

update Post set 
    name = 'Hibernate Master Class Training Material' 
where id = 1

update Comment set 
    post_id = 1, 
    review='Keep up the good work!' 
where id = 2

级联一对多删除操作

删除Post实体后,关联的Comment实体也将被删除:

Post post = newPost();

doInTransaction(session -> {
    session.delete(post);
});

生成以下输出:

delete from Comment where id = 1
delete from Comment where id = 2
delete from Post where id = 1

一对多删除孤立级联操作

移除孤儿使我们可以在父实体不再引用子实体时将其删除:

newPost();

doInTransaction(session -> {
    Post post = (Post) session.createQuery(
        "select p " +
                "from Post p " +
                "join fetch p.comments " +
                "where p.id = :id")
        .setParameter("id", 1L)
        .uniqueResult();
    post.removeComment(post.getComments().get(0));
});

正如我们在以下输出中看到的,评论已删除:

SELECT onetomanyc0_.id    AS id1_1_0_,
       comments1_.id      AS id1_0_1_,
       onetomanyc0_.NAME  AS name2_1_0_,
       comments1_.post_id AS post_id3_0_1_,
       comments1_.review  AS review2_0_1_,
       comments1_.post_id AS post_id3_1_0__,
       comments1_.id      AS id1_0_0__
FROM   post onetomanyc0_
INNER JOIN comment comments1_
    ON onetomanyc0_.id = comments1_.post_id
WHERE  onetomanyc0_.id = 1

delete from Comment where id = 1

多对多

多对多关系是棘手的,因为此关联的每一方都扮演“ 父母”和“ 孩子”角色。 尽管如此,我们仍可以从我们要传播实体状态更改的地方识别出一侧。

我们不应该默认使用CascadeType.ALL ,因为CascadeTpe.REMOVE最终可能会删除比我们期望的更多的内容(您很快就会发现):

@Entity
public class Author {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    @Column(name = "full_name", nullable = false)
    private String fullName;

    @ManyToMany(mappedBy = "authors", 
        cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private List<Book> books = new ArrayList<>();

    private Author() {}

    public Author(String fullName) {
        this.fullName = fullName;
    }

    public Long getId() {
        return id;
    }

    public void addBook(Book book) {
        books.add(book);
        book.authors.add(this);
    }

    public void removeBook(Book book) {
        books.remove(book);
        book.authors.remove(this);
    }

    public void remove() {
        for(Book book : new ArrayList<>(books)) {
            removeBook(book);
        }
    }
}

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    @Column(name = "title", nullable = false)
    private String title;

    @ManyToMany(cascade = 
        {CascadeType.PERSIST, CascadeType.MERGE})
    @JoinTable(name = "Book_Author",
        joinColumns = {
            @JoinColumn(
                name = "book_id", 
                referencedColumnName = "id"
            )
        },
        inverseJoinColumns = {
            @JoinColumn(
                name = "author_id", 
                referencedColumnName = "id"
            )
        }
    )
    private List<Author> authors = new ArrayList<>();

    private Book() {}

    public Book(String title) {
        this.title = title;
    }
}

级联多对多持久操作

坚持作者实体也将保留书籍

Author _John_Smith = new Author("John Smith");
Author _Michelle_Diangello = 
    new Author("Michelle Diangello");
Author _Mark_Armstrong = 
    new Author("Mark Armstrong");

Book _Day_Dreaming = new Book("Day Dreaming");
Book _Day_Dreaming_2nd = 
    new Book("Day Dreaming, Second Edition");

_John_Smith.addBook(_Day_Dreaming);
_Michelle_Diangello.addBook(_Day_Dreaming);

_John_Smith.addBook(_Day_Dreaming_2nd);
_Michelle_Diangello.addBook(_Day_Dreaming_2nd);
_Mark_Armstrong.addBook(_Day_Dreaming_2nd);

session.persist(_John_Smith);
session.persist(_Michelle_Diangello);
session.persist(_Mark_Armstrong);

BookBook_Author行与Authors一起插入:

insert into Author (id, full_name) 
values (default, 'John Smith')

insert into Book (id, title) 
values (default, 'Day Dreaming')

insert into Author (id, full_name) 
values (default, 'Michelle Diangello')

insert into Book (id, title) 
values (default, 'Day Dreaming, Second Edition')

insert into Author (id, full_name) 
values (default, 'Mark Armstrong')

insert into Book_Author (book_id, author_id) values (1, 1)
insert into Book_Author (book_id, author_id) values (1, 2)
insert into Book_Author (book_id, author_id) values (2, 1)
insert into Book_Author (book_id, author_id) values (2, 2)
insert into Book_Author (book_id, author_id) values (3, 1)

解除多对多关联的一侧

要删除Author ,我们需要取消关联属于可移动实体的所有Book_Author关系:

doInTransaction(session -> {
    Author _Mark_Armstrong =
        getByName(session, "Mark Armstrong");
    _Mark_Armstrong.remove();
    session.delete(_Mark_Armstrong);
});

该用例生成以下输出:

SELECT manytomany0_.id        AS id1_0_0_,
       manytomany2_.id        AS id1_1_1_,
       manytomany0_.full_name AS full_nam2_0_0_,
       manytomany2_.title     AS title2_1_1_,
       books1_.author_id      AS author_i2_0_0__,
       books1_.book_id        AS book_id1_2_0__
FROM   author manytomany0_
INNER JOIN book_author books1_
	ON manytomany0_.id = books1_.author_id
INNER JOIN book manytomany2_
	ON books1_.book_id = manytomany2_.id
WHERE  manytomany0_.full_name = 'Mark Armstrong'

SELECT books0_.author_id  AS author_i2_0_0_,
       books0_.book_id    AS book_id1_2_0_,
       manytomany1_.id    AS id1_1_1_,
       manytomany1_.title AS title2_1_1_
FROM   book_author books0_
INNER JOIN book manytomany1_
    ON books0_.book_id = manytomany1_.id
WHERE  books0_.author_id = 2

delete from Book_Author where book_id = 2

insert into Book_Author (book_id, author_id) values (2, 1)
insert into Book_Author (book_id, author_id) values (2, 2)

delete from Author where id = 3

多对多关联会生成太多冗余SQL语句,并且经常很难调整它们。 接下来,我将演示多对多CascadeType.REMOVE隐藏的危险。

多对多CascadeType.REMOVE陷阱

多对多CascadeType.ALL是另一个代码异味,我在查看代码时经常碰到。 所述CascadeType.REMOVE使用CascadeType.ALL时自动继承,但实体去除不仅应用到链接表,但对关联的另一侧为好。

让我们将Author实体书籍多对多关联更改为使用CascadeType.ALL代替:

@ManyToMany(mappedBy = "authors", 
    cascade = CascadeType.ALL)
private List<Book> books = new ArrayList<>();

删除一位作者时

doInTransaction(session -> {
    Author _Mark_Armstrong = 
        getByName(session, "Mark Armstrong");
    session.delete(_Mark_Armstrong);
    Author _John_Smith = 
        getByName(session, "John Smith");
    assertEquals(1, _John_Smith.books.size());
});

属于已删除作者的所有图书都将被删除,即使我们仍与已删除图书相关联的其他作者也是如此:

SELECT manytomany0_.id        AS id1_0_,
       manytomany0_.full_name AS full_nam2_0_
FROM   author manytomany0_
WHERE  manytomany0_.full_name = 'Mark Armstrong'  

SELECT books0_.author_id  AS author_i2_0_0_,
       books0_.book_id    AS book_id1_2_0_,
       manytomany1_.id    AS id1_1_1_,
       manytomany1_.title AS title2_1_1_
FROM   book_author books0_
INNER JOIN book manytomany1_ ON 
       books0_.book_id = manytomany1_.id
WHERE  books0_.author_id = 3  

delete from Book_Author where book_id=2
delete from Book where id=2
delete from Author where id=3

通常,此行为与业务逻辑期望不符,仅在首次删除实体时才发现。

如果我们也将CascadeType.ALL设置为Book实体,则可以进一步推动该问题:

@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "Book_Author",
    joinColumns = {
        @JoinColumn(
            name = "book_id", 
            referencedColumnName = "id"
        )
    },
    inverseJoinColumns = {
        @JoinColumn(
            name = "author_id", 
            referencedColumnName = "id"
        )
    }
)

这次,不仅书籍被删除,而且作者也被删除:

doInTransaction(session -> {
    Author _Mark_Armstrong = 
        getByName(session, "Mark Armstrong");
    session.delete(_Mark_Armstrong);
    Author _John_Smith = 
        getByName(session, "John Smith");
    assertNull(_John_Smith);
});

作者的删除触发所有相关书籍的删除,这进一步触发所有相关的作者的删除。 这是一个非常危险的操作,会导致大规模实体删除,这很少是预期的行为。

SELECT manytomany0_.id        AS id1_0_,
       manytomany0_.full_name AS full_nam2_0_
FROM   author manytomany0_
WHERE  manytomany0_.full_name = 'Mark Armstrong'  

SELECT books0_.author_id  AS author_i2_0_0_,
       books0_.book_id    AS book_id1_2_0_,
       manytomany1_.id    AS id1_1_1_,
       manytomany1_.title AS title2_1_1_
FROM   book_author books0_
INNER JOIN book manytomany1_
   ON books0_.book_id = manytomany1_.id
WHERE  books0_.author_id = 3  

SELECT authors0_.book_id      AS book_id1_1_0_,
       authors0_.author_id    AS author_i2_2_0_,
       manytomany1_.id        AS id1_0_1_,
       manytomany1_.full_name AS full_nam2_0_1_
FROM   book_author authors0_
INNER JOIN author manytomany1_
   ON authors0_.author_id = manytomany1_.id
WHERE  authors0_.book_id = 2  

SELECT books0_.author_id  AS author_i2_0_0_,
       books0_.book_id    AS book_id1_2_0_,
       manytomany1_.id    AS id1_1_1_,
       manytomany1_.title AS title2_1_1_
FROM   book_author books0_
INNER JOIN book manytomany1_
   ON books0_.book_id = manytomany1_.id
WHERE  books0_.author_id = 1 

SELECT authors0_.book_id      AS book_id1_1_0_,
       authors0_.author_id    AS author_i2_2_0_,
       manytomany1_.id        AS id1_0_1_,
       manytomany1_.full_name AS full_nam2_0_1_
FROM   book_author authors0_
INNER JOIN author manytomany1_
   ON authors0_.author_id = manytomany1_.id
WHERE  authors0_.book_id = 1  

SELECT books0_.author_id  AS author_i2_0_0_,
       books0_.book_id    AS book_id1_2_0_,
       manytomany1_.id    AS id1_1_1_,
       manytomany1_.title AS title2_1_1_
FROM   book_author books0_
INNER JOIN book manytomany1_
   ON books0_.book_id = manytomany1_.id
WHERE  books0_.author_id = 2  

delete from Book_Author where book_id=2
delete from Book_Author where book_id=1
delete from Author where id=2
delete from Book where id=1
delete from Author where id=1 
delete from Book where id=2
delete from Author where id=3

这种用例在很多方面都是错误的。 大量不必要的SELECT语句,最终我们最终删除了所有作者及其所有书籍。 这就是为什么当您在多对多关联中发现CascadeType.ALL时,它应该引起您的注意。

当涉及到Hibernate映射时,您应该始终追求简单性。 Hibernate文档也证实了这一假设:

真正的多对多关联的实际测试案例很少见。 大多数时候,您需要存储在“链接表”中的其他信息。 在这种情况下,最好将两个一对多关联用于中间链接类。 实际上,大多数关联是一对多和多对一的。 因此,在使用任何其他关联样式时,您应谨慎进行。

结论

级联是一种方便的ORM功能,但并非没有问题。 您应该仅从父级实体级联到子级,而不是相反。 您应该始终仅使用业务逻辑要求所要求的Casacde操作,而不应将CascadeType.ALL转换为默认的Parent-Child关联实体状态传播配置。

翻译自: https://www.javacodegeeks.com/2015/03/a-beginners-guide-to-jpa-and-hibernate-cascade-types.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值