Рассмотрим настройку 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, то при удалении комментария из списка комментариев топика, ОН удаляется из базы.»
В данном предложении не совсем ясно, «ОН» — это применительно к топику или к комментарию.
комментарию.
Шикарная статья