CascadeType.ОТСОЕДИНЕНИЕ не распространяется через связь @ManyToMany в режиме гибернации

#spring #hibernate #spring-data-jpa #hibernate-mapping #cascade

#spring #режим гибернации #spring-data-jpa #отображение в режиме гибернации #каскад

Вопрос:

Я пытаюсь клонировать объект post. Для этого я устанавливаю его идентификатор на null , затем отсоединяю его с помощью entityManager , а затем сохраняю.

Что касается комментариев к сообщению, я разрешаю CascadeType.DETACH выполнять работу по распространению изменений. Рассмотрим следующий фрагмент кода, чтобы проиллюстрировать проблему:

 // PostService.java
private void clonePost(Post post) {
  post.setId(null);
  post.getComments().forEach(comment -> comment.setId(null));

  entityManager.detach(post); // CascadeType will take care of detaching the comments
  postRepository.save(post);  // now we have a duplicated post with the same comments

  // Since it's a detached entity and has no ID, Hibernate will treat it as a 
  // new entity and save it creating a new record
}
  

Теперь есть одна загвоздка. Вот Post класс:

 public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @SequenceGenerator(allocationSize = 50, initialValue = 1)
    private Long id;
    
    // ...
    
    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> comments = new ArrayList<>();
    
    // ...
    
    @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.DETACH)
    @JoinTable(name = "post_tags", 
          joinColumns = @JoinColumn(name = "post_fk", referencedColumnName = "id"), 
          inverseJoinColumns = @JoinColumn(name = "post_tag_fk", referencedColumnName = "id"))
    @Fetch(FetchMode.SUBSELECT)
    private Set<PostTag> tags = new HashSet<>();
}
  

Сообщение tags использует @ManyToMany однонаправленную связь, к которой Post относится владелец, а также выполняется ленивая выборка. Обычно отсоединение post также приводит к отсоединению тегов, потому что cascade = CascadeType.DETACH установлено в @ManyToMany .

Однако этого не происходит. Теги не отсоединяются. Похоже, что лень имеет какой-то эффект, который препятствует адекватному распространению detach, поскольку выполнение этого вручную или инициализация тегов устраняет проблему:

 private void clonePost(Post post) {
    // ...
    
    // option 1 - manual detaching
    post.getTags().forEach(tag -> entityManager.detach(tag)); 
    
    // option 2 - initializing tags allows the detach of the post to propagate to them
    post.getTags().forEach(tag -> Hibernate.initialize(tag));

    entityManager.detach(post);
    postRepository.save(post);
}
  

Обоих вариантов можно избежать, если теги настроены на быструю загрузку:

 public class Post {
    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.DETACH)
    @JoinTable(name = "post_tags", 
          joinColumns = @JoinColumn(name = "post_fk", referencedColumnName = "id"), 
          inverseJoinColumns = @JoinColumn(name = "post_tag_fk", referencedColumnName = "id"))
    // @Fetch(FetchMode.JOIN) // this fetch mode will also make the tags to load eager
    private Set<PostTag> tags = new HashSet<>();
}
  

Почему это происходит? Почему не CascadeType.DETACH распространяется на лениво загруженную @ManyToMany коллекцию, когда родительский элемент отсоединен?

Ответ №1:

Я думаю, это происходит по той же причине, что и для REMOVE . В документации по гибернации говорится:

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

Например, если @ManyToMany(cascade = CascadeType.ALL) было определено и первое лицо было бы удалено, Hibernate выдал бы исключение, потому что другое лицо все еще связано с удаляемым адресом.

 Person person1 = entityManager.find(Person.class, personId);
entityManager.remove(person1);

Caused by: javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
Caused by: java.sql.SQLIntegrityConstraintViolationException: integrity constraint violation: foreign key no action; FKM7J0BNABH2YR0PE99IL1D066U table: PERSON_ADDRESS
  

Комментарии:

1. Спасибо за ваш ответ, @SternK. Насколько я понимаю, это REMOVE не имеет такого же поведения, как DETACH . Фактически, если коллекция настроена на быструю загрузку, DETACH распространение работает так, как ожидалось. Это когда коллекция загружается лениво, что она не распространяется.