Автор оригинала: Vlad Mihalcea.
JPA переводит переходы состояния сущности в базу данных DML операторы. Поскольку обычно используется для работы с графами сущностей, JPA позволяет нам распространять изменения состояния сущностей от Родителей к Дочерним сущностям.
Это поведение настраивается с помощью сопоставлений CascadeType .
Hibernate поддерживает все типы каскадов JPA и некоторые дополнительные устаревшие стили каскадирования. В следующей таблице показана связь между типами JPA Каскад и их Гибернацией собственным API эквивалентом:
Прослушиватель событий выселения по умолчанию | ОТСОЕДИНИТЬ | отсоединить(сущность) | ОТСОЕДИНИТЬ или ВЫСЕЛИТЬ | выселить(юридическое лицо) |
Прослушиватель событий слияния по умолчанию | ПОГЛОЩАТЬ | слияние(сущность) | ПОГЛОЩАТЬ | слияние(сущность) |
Прослушиватель постоянных событий по умолчанию | НАСТАИВАТЬ | сохраниться(сущность) | НАСТАИВАТЬ | сохраниться(сущность) |
Прослушиватель событий Обновления по умолчанию | ОСВЕЖИТЬ | обновить(сущность) | ОСВЕЖИТЬ | обновить(сущность) |
Прослушиватель событий Удаления по умолчанию | УДАЛИТЬ | удалить(сущность) | УДАЛИТЬ или УДАЛИТЬ | удалить(сущность) |
Прослушиватель Событий Сохранения Или Обновления По Умолчанию | СОХРАНИТЬ ОБНОВЛЕНИЕ | Сохранить обновление(сущность) | ||
Прослушиватель событий Репликации по Умолчанию | КОПИРОВАТЬ | репликация(сущность, режим репликации) | ||
Прослушиватель событий Блокировки по умолчанию | блокировка(сущность, тип блокировки) | ЗАМОК | buildLockRequest(сущность, блокировки) | |
ВСЕ | Все вышеперечисленные методы EntityManager | ВСЕ | Все вышеперечисленные методы сеанса гибернации |
Из этой таблицы мы можем сделать вывод, что:
- Нет никакой разницы между вызовом сохраниться , объединить или обновить в JPA EntityManager или Спящий режим | Сеанс .
- JPA удалить и отсоединить вызовы делегируются Спящий режим |/удаление и удаление собственных операций. Только Спящий режим
поддерживает репликацию и Сохранение/обновление . Хотя репликация полезна для некоторых очень специфических сценариев (когда точное состояние сущности должно быть отражено между двумя различными источниками данных ), комбинация сохранение и слияние всегда является лучшей альтернативой, чем собственная операция saveOrUpdate . Как правило, вы всегда должны использовать persist
для ПЕРЕХОДНЫХ сущностей и объединять для ОТДЕЛЕННЫХ сущностей. Недостатки saveOrUpdate
(при передаче снимка отсоединенной сущности в Сеанс , уже управляющий этой сущностью) привели к слиянию предшественнику операции: ныне исчезнувшей saveOrUpdateCopy операции. Метод JPA
- блокировки ведет себя так же, как и метод запроса Hibernate блокировки. Каскадный тип JPA
|/.ВСЕ применимо не только к EntityManager операциям изменения состояния, но и ко всем Спящий режим Каскадные типы также. Поэтому, если вы сопоставили свои ассоциации с каскадным типом.ВСЕ , вы все еще можете каскадировать
Спящий режим конкретные события. Например, вы можете каскадировать операцию блокировки JPA (хотя она ведет себя как повторное подключение, а не как фактическое распространение запроса на блокировку), даже если 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(PostDetails details) { if (details != null) { details.setPost(null); } this.details = null; } } @Entity public class PostDetails { @Id private Long id; @Column(name = "created_on") @Temporal(TemporalType.TIMESTAMP) private Date createdOn = new Date(); private boolean visible; @OneToOne @MapsId 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 играет Родительскую роль, а Сведения о публикации является Дочерним .
Двунаправленные ассоциации всегда должны обновляться с обеих сторон, поэтому Родительская сторона должна содержать комбинацию addChild и removeChild . Эти методы гарантируют, что мы всегда синхронизируем обе стороны связи, чтобы избежать проблем с повреждением объектов или реляционных данных.
В данном конкретном случае тип каскадный.ВСЕ и orphanremoval имеют смысл, потому что Сведения о публикации жизненный цикл привязан к Публикации Родительской сущности.
Каскадирование операции сохранения один к одному
Тип каскада .СОХРАНЯЕМЫЙ поставляется вместе с типом каскада.ВСЯ конфигурация , поэтому нам нужно сохранить только сущность 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 (1, '2015-03-03 10:17:19.14', false)
Каскадирование операции слияния один к одному
Тип каскада .СЛИЯНИЕ наследуется от каскадного типа.ВСЕ настройки , поэтому нам нужно только объединить сущность 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.ВСЕ конфигурация, поэтому удаление Записи сущности также вызывает удаление записей сущности:
Post post = newPost(); doInTransaction(session -> { session.delete(post); });
Создание следующего вывода:
delete from PostDetails where id = 1 delete from Post where id = 1
Каскадная операция удаления сирот один к одному
Если Дочерняя сущность отделена от своего Родителя , Дочерний внешний ключ имеет значение NULL . Если мы также хотим удалить Дочернюю строку, мы должны использовать поддержку orphanremoval .
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 ), поскольку Дочерняя сущность контролирует ассоциацию через свой Внешний ключ. Но каскад не ограничивается двунаправленными ассоциациями, мы также можем использовать его для однонаправленных отношений:
@Entity public class Commit { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String comment; @OneToOne(cascade = CascadeType.ALL) 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 Listcomments = new ArrayList<>(); public void setName(String name) { this.name = name; } public List 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; } }
Как в примере один к одному , тип каскада .ВСЕ и orphanremoval подходят, потому что Комментарий жизненный цикл привязан к Сообщению |/Родительской сущности.
Каскадирование операции сохранения “один ко многим”
Нам нужно только сохранить Запись сущность, и все связанные Комментарии сущности также сохраняются:
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 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 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
Каскадная операция “один ко многим” по удалению сироты
orphanremoval позволяет нам удалять Дочернюю сущность всякий раз, когда на нее больше не ссылается ее Родитель :
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.ВСЕ потому что CascadeType.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 Listbooks = 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.getAuthors().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 authors = new ArrayList<>(); private Book() {} public Book(String title) { this.title = title; } public List getAuthors() { retrun authors; } }
Каскадирование операции сохранения “многие ко многим”
Сохранение Автора сущностей также сохранит Книги :
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_Author строки вставляются вместе с Авторами :
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 (2, 3)
Разъединение одной стороны ассоциации “многие ко многим”
Чтобы удалить Автора , нам нужно отделить все 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.ВСЕ , но удаление сущности применяется не только к таблице ссылок, но и к другой стороне ассоциации.
Давайте изменим ассоциацию Автор сущность книги |/многие ко многим , чтобы использовать Каскадный тип.ВСЕ вместо:
@ManyToMany(mappedBy = "authors", cascade = CascadeType.ALL) private Listbooks = 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
Чаще всего такое поведение не соответствует ожиданиям бизнес-логики и обнаруживается только при первом удалении сущности.
Мы можем продвинуть эту проблему еще дальше, если установим Каскадный тип.ВСЕ на сторону Книги сущности, а также:
@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 утверждений, и в конечном итоге мы удаляем всех авторов и все их книги. Вот почему Каскадный тип.ВСЕ должны поднимать брови всякий раз, когда вы замечаете это в ассоциации “многие ко многим”//.
Когда дело доходит до Спящий режим сопоставления, вы всегда должны стремиться к простоте. Документация Hibernate также подтверждает это предположение:
Практические тестовые примеры для реальных многие ко многим ассоциаций встречаются редко. Большую часть времени вам нужна дополнительная информация, хранящаяся в таблице ссылка . В этом случае гораздо лучше использовать две ассоциации один ко многим для класса промежуточной связи. На самом деле, большинство ассоциаций являются один ко многим и многие к одному . По этой причине вам следует действовать осторожно при использовании любого другого стиля ассоциации.
Каскадирование-удобная функция ORM , но она не свободна от проблем. Вы должны переходить только от родительских сущностей к Дочерним , а не наоборот. Вы всегда должны использовать только операции casacde, которые требуются в соответствии с требованиями вашей бизнес-логики, а не включать каскадный тип.ВСЕ в конфигурацию распространения состояния объекта ассоциации по умолчанию Родительский – Дочерний .
Код доступен на GitHub .