Как работает orphanRemoval

Рассмотрим настройку 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, надо представить, что теоретически может подразумеваться под удалением комментария из списка комментариев топика.

Очевидно это означает, что у данного топика больше нет комментария.

  1. Но остается ли он вообще в базе, то есть можно ли его вывести в общем списке комментариев всех топиков?
  2. Или же комментарий удаляется из базы?

За эти два варианта и отвечает 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 разрешает «удаление сирот» — так можно запомнить смысл значения переключателя.

Как работает orphanRemoval: 6 комментариев

  1. У вас определенно талант. Понравилось ваше объяснение, все по делу. Спасибо!

  2. Большое спасибо за Ваши старания. Всё доступно и понятно разъяснено, причем с примерами и в сравнении с Cascade!

  3. Огромное спасибо за материалы! Очень помогает в продвижении по проекту.
    Небольшое замечание по тексту. Цитата:
    «Если orphanRemoval=true, то при удалении комментария из списка комментариев топика, ОН удаляется из базы.»
    В данном предложении не совсем ясно, «ОН» — это применительно к топику или к комментарию.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *