Статьи

Руководство для начинающих по JPA и Hibernate Cascade Types

Вступление

JPA переводит переходы состояния сущности в операторы базы данных DML . Поскольку работать с графами сущностей обычно, JPA позволяет нам распространять изменения состояния сущностей с родительских на дочерние сущности.

Это поведение настраивается с помощью сопоставлений CascadeType .

JPA против Hibernate Каскад Типы

Hibernate поддерживает все каскадные типы JPA и некоторые дополнительные устаревшие каскадные стили. В следующей таблице показана связь между типами JPA Cascade и их эквивалентом в Hibernate:

JPA EntityManager action JPA CascadeType Hibernate родной Session action Спящий родной CascadeType Слушатель событий
открепление (юридическое лицо) DETACH выселить (юридическое лицо) DETACH или

выселять
Прослушиватель событий Evict по умолчанию
Слияние (организация) MERGE Слияние (организация) MERGE Прослушиватель событий слияния по умолчанию
упорствовать (юридическое лицо) PERSIST упорствовать (юридическое лицо) PERSIST Постоянный прослушиватель событий по умолчанию
обновления (юридическое лицо) ОБНОВЛЕНИЕ обновления (юридическое лицо) ОБНОВЛЕНИЕ Обновление прослушивателя событий по умолчанию
удалить (юридическое лицо) УДАЛЯТЬ удаление (юридическое лицо) УДАЛИТЬ или УДАЛИТЬ Прослушиватель событий удаления по умолчанию
saveOrUpdate (юридическое лицо) SAVE_UPDATE Сохранить или обновить прослушиватель событий по умолчанию
replicate (entity, replicationMode) REPLICATE Прослушиватель событий репликации по умолчанию
блокировка (entity, lockModeType) buildLockRequest (entity, lockOptions) ЗАМОК Прослушиватель событий блокировки по умолчанию
Все вышеперечисленные методы EntityManager ВСЕ Все вышеперечисленные методы Hibernate Session ВСЕ

Из этой таблицы можно сделать вывод, что:

  • Нет разницы между вызовами persist , merge или refresh в JPA EntityManager или Hibernate Session .
  • Вызовы JPA для удаления и отсоединения делегируются Hibernate для удаления и удаления собственных операций.
  • Только Hibernate поддерживает репликацию и saveOrUpdate . В то время как репликация полезна для некоторых очень специфических сценариев (когда точное состояние объекта должно быть отражено между двумя различными источниками данных), комбинация persist и merge всегда является лучшей альтернативой, чем собственная операция saveOrUpdate. Как правило, вы должны всегда используйте постоянство для объектов TRANSIENT и объединение для объектов DETACHED. Недостатки saveOrUpdate (при передаче снимка отдельного объекта сеансу, уже управляющему этим объектом) привели к предшественнику операции объединения : теперь уже потухшая операция saveOrUpdateCopy .
  • Метод блокировки JPA использует то же поведение, что и метод запроса блокировки Hibernate.
  • JPA CascadeType.ALL применяется не только к операциям изменения состояния EntityManager , но и ко всем Hibernate CascadeTypes. Так что если вы сопоставили свои ассоциации с CascadeType.ALL , вы все равно можете каскадировать определенные события Hibernate. Например, вы можете каскадировать операцию блокировки JPA (хотя она ведет себя как присоединение, а не фактическое распространение запроса блокировки), даже если JPA не определяет LOCK CascadeType .

Каскадные лучшие практики

Каскадирование имеет смысл только для родительско- дочерних ассоциаций (переход состояния родительского объекта каскадно связан с его дочерними объектами). Каскадирование от Ребенка к Родителю не очень полезно и, как правило, это запах кода отображения.

Далее я собираюсь проанализировать каскадное поведение всех ассоциаций родительского и дочернего JPA.

Один к одному

Наиболее распространенная двусторонняя ассоциация «один к одному» выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@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, а PostDetails — это Child .

Двунаправленные ассоциации всегда должны обновляться с обеих сторон, поэтому родительская сторона должна содержать комбинацию addChild и removeChild . Эти методы гарантируют, что мы всегда синхронизируем обе стороны ассоциации, чтобы избежать проблем повреждения данных объекта или реляционной.

В этом конкретном случае удаление CascadeType.ALL и потерянных объектов имеет смысл, поскольку жизненный цикл PostDetails связан с жизненным циклом его объекта Post Parent .

Каскадная персистентная операция один-к-одному

CascadeType.PERSIST поставляется вместе с конфигурацией CascadeType.ALL , поэтому нам нужно только сохранить сущность Post , и связанная сущность PostDetails также сохраняется:

1
2
3
4
5
6
7
8
Post post = new Post();
post.setName("Hibernate Master Class");
 
PostDetails details = new PostDetails();
 
post.addDetails(details);
 
session.persist(post);

Генерация следующего вывода:

1
2
3
4
5
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 :

1
2
3
4
5
6
7
Post post = newPost();
post.setName("Hibernate Master Class Training Material");
post.getDetails().setVisible(true);
 
doInTransaction(session -> {
    session.merge(post);
});

Операция объединения генерирует следующий вывод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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 :

1
2
3
4
5
Post post = newPost();
 
doInTransaction(session -> {
    session.delete(post);
});

Генерация следующего вывода:

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

Один-к-одному удалить потерянную каскадную операцию

Если дочерний объект отделен от своего родительского объекта, для дочернего внешнего ключа устанавливается значение NULL. Если мы также хотим удалить дочернюю строку, мы должны использовать поддержку удаления потерянных объектов .

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

Удаление сирот генерирует этот вывод:

01
02
03
04
05
06
07
08
09
10
11
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 ), а дочерний объект контролирует ассоциацию через свой внешний ключ. Но каскад не ограничивается двунаправленными ассоциациями, мы также можем использовать его для однонаправленных связей:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@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;
    }
}

Каскадирование заключается в распространении перехода состояния родительского объекта на один или несколько дочерних объектов, и его можно использовать как для однонаправленных, так и для двунаправленных ассоциаций.

Один ко многим

Наиболее распространенная ассоциация РодительРебенок состоит из отношений «один ко многим» и «многие к одному», где каскад полезен только для стороны «один ко многим»:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@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 также будут сохранены:

01
02
03
04
05
06
07
08
09
10
11
12
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);

Операция persist генерирует следующий вывод:

1
2
3
4
5
6
7
8
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 :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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);
});

Генерация следующего вывода:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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 также удаляются:

1
2
3
4
5
Post post = newPost();
 
doInTransaction(session -> {
    session.delete(post);
});

Генерация следующего вывода:

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

Каскадная операция удаления «один ко многим»

Удаление-сирота позволяет нам удалять сущность Child, когда на нее больше не ссылается родительский объект:

01
02
03
04
05
06
07
08
09
10
11
12
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));
});

Комментарий удален, как мы видим в следующем выводе:

01
02
03
04
05
06
07
08
09
10
11
12
13
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 может в конечном итоге удалить больше, чем мы ожидаем (как вы скоро узнаете):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@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;
    }
}

Каскадная операция «многие ко многим»

Сохранение сущностей Автора также сохранит Книги :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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 вставляются вместе с авторами :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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)

Разобщение одной стороны ассоциации «многие ко многим»

Чтобы удалить Автора , нам нужно отделить все отношения Book_Author, принадлежащие съемному объекту:

1
2
3
4
5
6
doInTransaction(session -> {
    Author _Mark_Armstrong =
        getByName(session, "Mark Armstrong");
    _Mark_Armstrong.remove();
    session.delete(_Mark_Armstrong);
});

Этот вариант использования генерирует следующий вывод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 , но удаление сущностей применяется не только к таблице ссылок, но и к другой стороне ассоциации.

Давайте заменим ассоциацию «многие ко многим» книги « Автор», чтобы вместо нее использовать CascadeType.ALL :

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

При удалении одного автора :

1
2
3
4
5
6
7
8
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());
});

Все книги, принадлежащие удаленному автору , удаляются, даже если другие авторы все еще связаны с удаленными книгами :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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 :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "Book_Author",
    joinColumns = {
        @JoinColumn(
            name = "book_id",
            referencedColumnName = "id"
        )
    },
    inverseJoinColumns = {
        @JoinColumn(
            name = "author_id",
            referencedColumnName = "id"
        )
    }
)

На этот раз удаляются не только Книги , но и Авторы :

1
2
3
4
5
6
7
8
doInTransaction(session -> {
    Author _Mark_Armstrong =
        getByName(session, "Mark Armstrong");
    session.delete(_Mark_Armstrong);
    Author _John_Smith =
        getByName(session, "John Smith");
    assertNull(_John_Smith);
});

Удаление Автор инициирует удаление всех связанных Книг , что дополнительно вызывает удаление всех связанных Авторов . Это очень опасная операция, в результате которой происходит массовое удаление объектов, что редко приводит к ожидаемому поведению.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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 в стандартную конфигурацию распространения состояния сущности «родитель-потомок».

  • Код доступен на GitHub .