Рубрики
Без рубрики

Как orphanRemoval работает с JPA и гибернацией

Узнайте, как работает механизм orphanRemoval при использовании JPA и Hibernate и как он запускает операцию удаления дочернего элемента, когда дочерний элемент отключен.

Автор оригинала: Vlad Mihalcea.

Вступление

В этой статье мы рассмотрим, как механизм JPA и Hibernate orphanRemoval позволяет нам запускать операцию дочернего объекта удалить при отсоединении ссылки на дочернюю сущность от дочерней коллекции на родительской стороне.

Модель предметной области

Мы собираемся использовать Сообщение и Комментарий к сообщению сущность, которая образует отношение таблицы “один ко многим” :

@ManyToOne аннотация в Комментарии к сообщению сущности отображает столбец post_id внешнего ключа, который образует отношение таблицы “один ко многим” между родительской публикацией и post_comment дочерними таблицами:

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    private String review;

    //Getters and setters omitted for brevity
}

И объект Post отображается следующим образом:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @OneToMany(
        mappedBy = "post",
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private List comments = new ArrayList<>();

    //Getters and setters omitted for brevity

    public Post addComment(PostComment comment) {
        comments.add(comment);
        comment.setPost(this);
        return this;
    }

    public Post removeComment(PostComment comment) {
        comments.remove(comment);
        comment.setPost(null);
        return this;
    }
}

Коллекция комментариев сопоставляется с помощью аннотации @OneToMany , а атрибут mappedBy указывает поставщику JPA, что свойство post в дочерней сущности PostComment управляет базовым столбцом внешнего ключа.

Атрибут cascade имеет значение Тип каскада.ВСЕ , что означает, что все переходы состояния JPA и спящего режима сущности (например, сохранение , слияние , удаление ) передаются от родительской Последующей сущности к последующей дочерним сущностям.

Атрибут orphanRemoval даст указание поставщику JPA инициировать переход в состояние удаления сущности, когда PostComment сущность больше не ссылается на свою родительскую Post сущность.

Поскольку у нас есть двунаправленная связь “один ко многим”, нам нужно убедиться, что обе стороны связи синхронизированы, и по этой причине мы создали методы AddComment и removeComment для синхронизации обоих концов при добавлении или удалении новой дочерней сущности. Ознакомьтесь с этой статьей для получения более подробной информации.

JPA и каскадный тип спящего режима.Механизм СОХРАНЕНИЯ

Давайте создадим Сообщение сущность с двумя Комментарием к сообщению дочерними сущностями:

Post post = new Post()
    .setTitle("High-Performance Java Persistence")
    .addComment(
        new PostComment()
            .setReview("Best book on JPA and Hibernate!")
    )
    .addComment(
        new PostComment()
            .setReview("A must-read for every Java developer!")
    );


entityManager.persist(post);

Потому что каскадный тип.ВСЯ стратегия включает в себя каскадный тип.Опция PERSIST при вызове persist и сущности post Hibernate сохранит как Post , так и две дочерние сущности PostComment//, как показано в сгенерированных инструкциях INSERT:

INSERT INTO post (
    title, 
    id
) 
VALUES (
    'High-Performance Java Persistence', 
    1
)

INSERT INTO post_comment (
    post_id, 
    review, 
    id
) 
VALUES (
    1, 
    'Best book on JPA and Hibernate!', 
    2
)

INSERT INTO post_comment (
    post_id, 
    review, 
    id
) 
VALUES (
    1, 
    'A must-read for every Java developer!', 
    3
)

JPA и механизм временного отключения в спящем режиме

Если мы загрузим Сообщение сущность вместе с ее двумя последующими дочерними сущностями и удалим первый Комментарий к сообщению :

Post post = entityManager.createQuery("""
    select p
    from Post p
    join fetch p.comments c
    where p.id = :id
    order by p.id, c.id
    """, Post.class)
.setParameter("id", postId)
.getSingleResult();

post.removeComment(post.getComments().get(0));

Hibernate выполнит следующие инструкции SQL:

SELECT 
    p.id as id1_0_0_, 
    c.id as id1_1_1_, 
    p.title as title2_0_0_, 
    c.post_id as post_id3_1_1_, 
    c.review as review2_1_1_
FROM 
    post p 
INNER JOIN 
    post_comment c ON p.id = c.post_id 
WHERE 
    p.id = 1
ORDER BY 
    p.id, 
    c.id

DELETE FROM 
    post_comment 
WHERE 
    id = 2

Поскольку метод удалить комментарий удаляет ссылку Комментарий к сообщению из коллекции комментариев , механизм orphanRemoval запустит удаление для объекта PostComment , и будет выполнена инструкция DELETE.

Если мы установим для атрибута orphanRemoval значение false :

@OneToMany(
    mappedBy = "post",
    cascade = CascadeType.ALL,
    orphanRemoval = false
)
private List comments = new ArrayList<>();

И повторите предыдущий тестовый случай, в котором вызывался метод удалить комментарий , Hibernate выполнил следующие инструкции SQL:

SELECT 
    p.id as id1_0_0_, 
    c.id as id1_1_1_, 
    p.title as title2_0_0_, 
    c.post_id as post_id3_1_1_, 
    c.review as review2_1_1_
FROM 
    post p 
INNER JOIN 
    post_comment c ON p.id = c.post_id 
WHERE 
    p.id = 1
ORDER BY 
    p.id, 
    c.id

UPDATE 
    post_comment 
SET 
    post_id = NULL, 
    review = 'Best book on JPA and Hibernate!' 
WHERE 
    id = 2

Таким образом, вместо инструкции DELETE вместо нее выполняется инструкция UPDATE, устанавливающая в столбце post_id значение NULL . Такое поведение вызвано следующей строкой в методе удалить комментарий :

comment.setPost(null);

Итак, если вы хотите удалить базовую дочернюю запись после удаления связанной сущности из дочерней коллекции в родительской сущности, вам следует установить для атрибута orphanRemoval значение true .

JPA и гибернация сиротского времени по сравнению с каскадным типом.УДАЛИТЬ

Очень распространенный вопрос заключается в том, чем механизм orphanRemoval отличается от стратегии CascadeType.REMOVE .

Если механизм orphanRemoval позволяет нам запустить операцию удаления для несвязанной дочерней сущности, стратегия CascadeType.REMOVE распространяет операцию удаления от родительской сущности на все дочерние сущности.

Потому что в коллекции комментарии используется Каскадный тип.ВСЕ , это означает, что он также наследует стратегию CascadeType.REMOVE .

Поэтому, если мы выполним следующий тестовый случай:

Post post = entityManager.createQuery("""
    select p
    from Post p
    join fetch p.comments
    where p.id = :id
    """, Post.class)
.setParameter("id", postId)
.getSingleResult();

entityManager.remove(post);

Hibernate выполнит три инструкции УДАЛЕНИЯ:

DELETE FROM 
    post_comment 
WHERE 
    id = 2

DELETE FROM 
    post_comment 
WHERE 
    id = 3

DELETE FROM 
    post 
WHERE 
    id = 1

Во-первых, дочерние строки удаляются, потому что, если мы сначала удалим строку post , будет вызвано исключение Нарушения ограничений , поскольку все еще будут post_comment строки, связанные с записью post , которую хотели удалить.

Не используйте CascadeType.УДАЛИТЕ с помощью @ManyToMany ассоциаций

Стратегия CascadeType.REMOVE полезна только для ассоциаций @OneToMany и @OneToOne . Если вы используете ассоциацию @ManyToMany , вам никогда не следует устанавливать тип каскада.ВСЕ значение атрибута , так как вы также унаследуете стратегию CascadeType.REMOVE .

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

При удалении элемента из коллекции @ManyToMany Hibernate создает инструкцию DELETE для записи таблицы объединения. Таким образом, он работает как orphanRemoval, но вместо распространения удаления на фактическую сущность, удаляемую из коллекции, он запускает инструкцию DELETE для дочерней строки в таблице объединения.

Для получения более подробной информации об этой теме ознакомьтесь с этой статьей .

Вывод

Стратегия orphanRemoval упрощает управление состоянием дочерней сущности, так как нам нужно только удалить дочернюю сущность из дочерней коллекции, и связанная дочерняя запись также удаляется.

В отличие от стратегии orphanRemoval, CascadeType.REMOVE распространяет операцию remove от родительской к дочерним сущностям, как если бы мы вручную вызывали remove для каждой дочерней сущности.