«1. Обзор

Будучи полнофункциональной инфраструктурой ORM, Hibernate отвечает за управление жизненным циклом постоянных объектов (сущностей), включая операции CRUD, такие как чтение, сохранение, обновление и удаление.

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

Мы используем JPA и делаем шаг назад и используем собственный API Hibernate только для тех функций, которые не стандартизированы в JPA.

2. Различные способы удаления объектов

Объекты могут быть удалены в следующих сценариях:

    С помощью EntityManager.remove Когда удаление выполняется каскадно из других экземпляров сущности Когда применяется orphanRemoval Путем выполнения оператора delete JPQL Выполняя нативные запросы. Применяя технику мягкого удаления (фильтрация обратимо удаленных объектов по условию в предложении @Where)

В оставшейся части статьи мы подробно рассмотрим эти моменты.

3. Удаление с помощью Entity Manager

Удаление с помощью EntityManager — самый простой способ удалить экземпляр сущности:

Foo foo = new Foo("foo");
entityManager.persist(foo);
flushAndClear();

foo = entityManager.find(Foo.class, foo.getId());
assertThat(foo, notNullValue());
entityManager.remove(foo);
flushAndClear();

assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());

В примерах этой статьи мы используем вспомогательный метод для сброса и очистки контекст персистентности, когда это необходимо:

void flushAndClear() {
    entityManager.flush();
    entityManager.clear();
}

После вызова метода EntityManager.remove предоставленный экземпляр переходит в удаленное состояние, и связанное с ним удаление из базы данных происходит при следующей очистке.

Обратите внимание, что удаленный экземпляр повторно сохраняется, если к нему применяется операция PERSIST. Распространенной ошибкой является игнорирование того факта, что операция PERSIST была применена к удаленному экземпляру (обычно потому, что он каскадируется из другого экземпляра во время сброса), потому что раздел 3.2.2 спецификации JPA предписывает, что такой экземпляр должен быть удален. упорствовал снова в таком случае.

Мы проиллюстрируем это, определив ассоциацию @ManyToOne между Foo и Bar:

@Entity
public class Foo {
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Bar bar;

    // other mappings, getters and setters
}

Когда мы удаляем экземпляр Bar, на который ссылается экземпляр Foo, который также загружается в контексте постоянства, экземпляр Bar не будет удален. из базы данных:

Bar bar = new Bar("bar");
Foo foo = new Foo("foo");
foo.setBar(bar);
entityManager.persist(foo);
flushAndClear();

foo = entityManager.find(Foo.class, foo.getId());
bar = entityManager.find(Bar.class, bar.getId());
entityManager.remove(bar);
flushAndClear();

bar = entityManager.find(Bar.class, bar.getId());
assertThat(bar, notNullValue());

foo = entityManager.find(Foo.class, foo.getId());
foo.setBar(null);
entityManager.remove(bar);
flushAndClear();

assertThat(entityManager.find(Bar.class, bar.getId()), nullValue());

Если на удаленный Bar ссылается Foo, операция PERSIST каскадируется от Foo к Bar, потому что ассоциация отмечена cascade = CascadeType.ALL, а удаление является незапланированным. Чтобы убедиться, что это происходит, мы можем включить уровень журнала трассировки для пакета org.hibernate и выполнить поиск таких записей, как незапланированное удаление объекта.

4. Каскадное удаление

Удаление может быть каскадным для дочерних сущностей при удалении родителей:

Bar bar = new Bar("bar");
Foo foo = new Foo("foo");
foo.setBar(bar);
entityManager.persist(foo);
flushAndClear();

foo = entityManager.find(Foo.class, foo.getId());
entityManager.remove(foo);
flushAndClear();

assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());
assertThat(entityManager.find(Bar.class, bar.getId()), nullValue());

Здесь bar удаляется, потому что удаление происходит каскадно из foo, так как объявлена ​​ассоциация для каскадирования всех операций жизненного цикла от Фу до Бара.

Обратите внимание, что каскадная операция REMOVE в ассоциации @ManyToMany почти всегда является ошибкой, потому что это приведет к удалению дочерних экземпляров, которые могут быть связаны с другими родительскими экземплярами. Это также относится к CascadeType.ALL, так как это означает, что все операции должны быть каскадными, включая операцию REMOVE.

5. Удаление сирот

Директива orphanRemoval объявляет, что связанные экземпляры объектов должны быть удалены, когда они отсоединяются от родителя, или, что то же самое, при удалении родителя.

Мы показываем это, определяя такую ​​ассоциацию между Bar и Baz:

@Entity
public class Bar {
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Baz> bazList = new ArrayList<>();

    // other mappings, getters and setters
}

Затем экземпляр Baz удаляется автоматически, когда он удаляется из списка родительского экземпляра Bar:

Bar bar = new Bar("bar");
Baz baz = new Baz("baz");
bar.getBazList().add(baz);
entityManager.persist(bar);
flushAndClear();

bar = entityManager.find(Bar.class, bar.getId());
baz = bar.getBazList().get(0);
bar.getBazList().remove(baz);
flushAndClear();

assertThat(entityManager.find(Baz.class, baz.getId()), nullValue());

Семантика операции orphanRemoval полностью аналогична операции REMOVE, применяемой непосредственно к затронутым дочерним экземплярам, ​​что означает, что операция REMOVE далее каскадируется на вложенные дочерние элементы. Как следствие, вы должны убедиться, что никакие другие экземпляры не ссылаются на удаленные (в противном случае они будут повторно сохранены).

6. Удаление с помощью оператора JPQL

Hibernate поддерживает операции удаления в стиле DML:

Foo foo = new Foo("foo");
entityManager.persist(foo);
flushAndClear();

entityManager.createQuery("delete from Foo where id = :id")
  .setParameter("id", foo.getId())
  .executeUpdate();

assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());

«

«Важно отметить, что операторы JPQL в стиле DML не влияют ни на состояние, ни на жизненный цикл экземпляров сущностей, которые уже загружены в контекст постоянства, поэтому рекомендуется выполнять их до загрузки затронутых сущностей.

7. Удаление с помощью собственных запросов

Foo foo = new Foo("foo");
entityManager.persist(foo);
flushAndClear();

entityManager.createNativeQuery("delete from FOO where ID = :id")
  .setParameter("id", foo.getId())
  .executeUpdate();

assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());

Иногда нам нужно вернуться к собственным запросам, чтобы добиться чего-то, что не поддерживается Hibernate или специфично для поставщика базы данных. Мы также можем удалять данные в базе данных с помощью нативных запросов:

К нативным запросам применяются те же рекомендации, что и к операторам в стиле JPA DML, т. е. нативные запросы не влияют ни на состояние, ни на жизненный цикл экземпляров сущностей, которые загружаются в контекст сохраняемости до выполнения запросов.

8. Мягкое удаление

Часто нежелательно удалять данные из базы данных из-за целей аудита и сохранения истории. В таких ситуациях мы можем применить технику, называемую обратимым удалением. По сути, мы просто помечаем строку как удаленную и фильтруем ее при извлечении данных.

Чтобы избежать большого количества избыточных условий в предложениях where во всех запросах, которые считывают обратимо удаляемые сущности, Hibernate предоставляет аннотацию @Where, которую можно поместить на сущность и которая содержит фрагмент SQL, который автоматически добавляется в SQL. запросы, созданные для этого объекта.

@Entity
@Where(clause = "DELETED = 0")
public class Foo {
    // other mappings

    @Column(name = "DELETED")
    private Integer deleted = 0;
    
    // getters and setters

    public void setDeleted() {
        this.deleted = 1;
    }
}

Чтобы продемонстрировать это, мы добавляем аннотацию @Where и столбец с именем DELETED к объекту Foo:

Foo foo = new Foo("foo");
entityManager.persist(foo);
flushAndClear();

foo = entityManager.find(Foo.class, foo.getId());
foo.setDeleted();
flushAndClear();

assertThat(entityManager.find(Foo.class, foo.getId()), nullValue());

Следующий тест подтверждает, что все работает так, как ожидалось:

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

Реализация этого руководства по удалению объектов с помощью Hibernate Tutorial доступна на Github. Это проект на основе Maven, поэтому его легко импортировать и запускать как есть.