JPA和Hibernate级联类型的初学者指南
介紹
JPA將實體狀態轉換轉換為數據庫DML語句。 由于對實體圖進行操作很常見,因此JPA允許我們將實體狀態更改從父級傳播到子級 。
通過CascadeType映射配置此行為。
JPA與Hibernate級聯類型
Hibernate支持所有JPA級聯類型和一些其他舊式級聯樣式。 下表繪制了JPA級聯類型與其等效的Hibernate本機API之間的關聯:
| 分離(實體) | 分離 | 逐出(實體) | 分離或 EVICT | 默認驅逐事件偵聽器 |
| 合并(實體) | 合并 | 合并(實體) | 合并 | 默認合并事件監聽器 |
| 堅持(實體) | 堅持 | 堅持(實體) | 堅持 | 默認的持久事件監聽器 |
| 刷新(實體) | 刷新 | 刷新(實體) | 刷新 | 默認刷新事件監聽器 |
| 刪除(實體) | 去掉 | 刪除(實體) | 刪除或刪除 | 默認刪除事件監聽器 |
| saveOrUpdate(實體) | SAVE_UPDATE | 默認的保存或更新事件監聽器 | ||
| 復制(實體,復制模式) | 復制 | 默認復制事件監聽器 | ||
| 鎖(實體,lockModeType) | buildLockRequest(實體,lockOptions) | 鎖 | 默認鎖定事件監聽器 | |
| 以上所有EntityManager方法 | 所有 | 以上所有的Hibernate Session方法 | 所有 |
從該表可以得出以下結論:
- 在JPA EntityManager或Hibernate Session上調用persist , merge或refresh沒有什么區別。
- JPA的remove和detach調用被委托給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@PrimaryKeyJoinColumnprivate 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角色,而PostDetails是Child 。
雙向關聯應始終在兩側進行更新,因此父級側應包含addChild和removeChild組合。 這些方法確保我們始終同步關聯的雙方,以避免對象或關系數據損壞問題。
在這種特定情況下,刪除CascadeType.ALL和孤立的孤島是有意義的,因為PostDetails生命周期與其后 父實體的生命周期綁定在一起。
進行一對一的持久化操作
CascadeType.PERSIST與CascadeType.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 = 1UPDATE postdetails SET created_on = '2015-03-03 10:20:53.874', visible = true WHERE id = 1UPDATE 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 = 1delete 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;@ManyToOneprivate 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 = 1update Post set name = 'Hibernate Master Class Training Material' where id = 1update 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 = 1delete 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);Book和Book_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 = 2delete from Book_Author where book_id = 2insert 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關聯實體狀態傳播配置。
- 代碼可在GitHub上獲得 。
翻譯自: https://www.javacodegeeks.com/2015/03/a-beginners-guide-to-jpa-and-hibernate-cascade-types.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的JPA和Hibernate级联类型的初学者指南的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 避免在ConcurrentHashMap
- 下一篇: 12351是什么电话?