Рассмотрим настройку orphanRemoval, которая касается удаления элементов из коллекции. У нас это будет удаление комментария из списка комментариев топика.

Модель
То есть продолжаем работать с теми же таблицами — топик и комментарии в отношении @OneToMany и @ManyToOne:
Класс Comment:
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String text;
@ManyToOne(fetch = FetchType.LAZY)
private Topic topic;
// getters/setters/constructors
}
Класс Topic:
@Entity
public class Topic {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String title;
@OneToMany(mappedBy = "topic", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments=new ArrayList<>();
public void addComment(Comment comment) {
comments.add(comment);
comment.setTopic(this);
}
public void removeComment(Comment comment) {
comments.remove(comment);
comment.setTopic(null);
}
// getters/setters/constructors
}
Обратите внимание на метод removeComment() :
- он удаляет комментарий из коллекции
- устанавливает его полю topic значение null
Так данные остаются согласованными. Hibernate автоматически не может обеспечить согласованность двусторонних (bidirectional) отношений, и надо делать это самостоятельно.
orphanRemoval — свойство внутри аннотации @OneToMany (см. выше).
Данные
В базу добавим один топик с тремя комментариями — этого достаточно, чтобы протестировать orphanRemoval.
insert into topic (id, title) values (-1,'title1'); insert into comment (id, text, topic_id) values (-4, 'text1', -1); insert into comment (id, text, topic_id) values (-5, 'text2', -1); insert into comment (id, text, topic_id) values (-6, 'text3', -1);
Чтобы понять смысл настройки orphanRemoval, надо представить, что теоретически может подразумеваться под удалением комментария из списка комментариев топика.
Очевидно это означает, что у данного топика больше нет комментария.
- Но остается ли он вообще в базе, то есть можно ли его вывести в общем списке комментариев всех топиков?
- Или же комментарий удаляется из базы?
За эти два варианта и отвечает orphanRemoval.
orphanRemoval=true
Если orphanRemoval=true, то при удалении комментария из списка комментариев топика, комментарий удаляется из базы. Проверим это в тесте:
@Test
@DisplayName("если orphanRomoval=true, то при удалении комментария из топика он удаляется из базы")
public void givenOrphanRomovalTrue_whenRemoveCommentFromTopic_thenItRemovedFromDatabase() {
Topic topic = topicRepository.getById(-1l);
topic.removeComment(topic.getComments().get(0));
Assertions.assertEquals(2, commentRepository.count());
}
Генерируется SQL:
select topic0_.id as id1_1_0_, comments1_.id as id1_0_1_,
topic0_.title as title2_1_0_,
comments1_.text as text2_0_1_, comments1_.topic_id as topic_id3_0_1_,
comments1_.topic_id as topic_id3_0_0__, comments1_.id as id1_0_0__
from topic topic0_ inner join comment comments1_
on topic0_.id=comments1_.topic_id
where topic0_.id=?
delete from comment where id=?
Как видите, тут оператор delete. Он и удаляет комментарий из базы.
orphanRemoval=false
Если orphanRemoval=false, то при удалении комментария из списка, в базе комментарий остается. Просто его внешний ключ (comment.topic_id) обнуляется, и больше комментарий не ссылается на топик.
Проверим это:
@Test
@DisplayName("если orphanRomoval=false, то при удалении комментария из топика остается в базе")
public void givenOrphanRomovalFalse_whenRemoveCommentFromTopic_thenItRemovedFromDatabase() {
Topic topic = topicRepository.getById(-1l);
topic.removeComment(topic.getComments().get(0));
Assertions.assertEquals(3, commentRepository.count());
}
SQL:
select topic0_.id as id1_1_0_, comments1_.id as id1_0_1_,
topic0_.title as title2_1_0_, comments1_.text as text2_0_1_,
comments1_.topic_id as topic_id3_0_1_, comments1_.topic_id as topic_id3_0_0__,
comments1_.id as id1_0_0__
from topic topic0_ inner join comment comments1_
on topic0_.id=comments1_.topic_id
where topic0_.id=?
update comment set text=?, topic_id=? where id=?
Здесь происходит обновление таблицы comment: столбцу topic_id присваивается значение NULL. Комментарий остается в базе, просто ни на какой топик он больше не ссылается.
Оператора delete нет.
orphanRemoval vs CascadeType.REMOVE
Иногда путают настройки orphanRemoval и CascadeType.REMOVE. Хотя CascadeType.REMOVE совсем о другом.
Все каскады просто повторяют действие, выполняемое с родительской сущностью: они проделывают его также с дочерними сущностями.
У нас родительская сущность — топик, дочерние сущности — комментарии.
CascadeType.REMOVE говорит о том, что при удалении топика надо также удалять его комментарии из базы.
Пример: удалим топик с id=-1l. У него три комментария, и эти три комментария — всё, что есть в таблице комментариев. Убедимся, что комментарии из базы тоже удаляются:
@Test
@DisplayName("если CascadeType=REMOVE, то при удалении из базы топика удаляются его комментарии")
public void givenCascadeTypeIsRemove_whenRemoveTopic_thenCommentsRemoved() {
Topic topic = topicRepository.getById(-1l);
topicRepository.delete(topic);
Assertions.assertEquals(0, commentRepository.count());
}
Генерируемый SQL:
select topic0_.id as id1_1_0_, comments1_.id as id1_0_1_,
topic0_.title as title2_1_0_, comments1_.text as text2_0_1_,
comments1_.topic_id as topic_id3_0_1_, comments1_.topic_id as topic_id3_0_0__,
comments1_.id as id1_0_0__
from topic topic0_ inner join comment comments1_
on topic0_.id=comments1_.topic_id
where topic0_.id=?
delete from comment where id=?
delete from comment where id=?
delete from comment where id=?
delete from topic where id=?
Здесь последним оператором delete удаляется топик, а первыми тремя — три комментария, которые относятся к этому топику.
Настройка orphanRemoval на исход операции не влияет.
Итоги
Мы рассмотрели смысл настройки orphanRemoval, и чем она отличается от CascadeType.REMOVE.
Исходный код примеров есть на GitHub.
P.S.
orphan переводится как «сирота». «Сиротой» тут считается комментарий, не относящийся ни к одному топику. Получается, что orphanRemoval =true разрешает «удаление сирот» — так можно запомнить смысл значения переключателя.
отличная статья , спасибо
У вас определенно талант. Понравилось ваше объяснение, все по делу. Спасибо!
Большое спасибо за Ваши старания. Всё доступно и понятно разъяснено, причем с примерами и в сравнении с Cascade!
Огромное спасибо за материалы! Очень помогает в продвижении по проекту.
Небольшое замечание по тексту. Цитата:
«Если orphanRemoval=true, то при удалении комментария из списка комментариев топика, ОН удаляется из базы.»
В данном предложении не совсем ясно, «ОН» — это применительно к топику или к комментарию.
комментарию.
Шикарная статья