Статьи

Эффективно удаляйте данные с помощью JPA и Hibernate

Вы можете столкнуться с ситуацией, когда вам нужно выполнить массовое удаление огромного количества наборов данных, хранящихся в реляционной базе данных. Если вы используете JPA с Hibernate в качестве базового преобразователя OR, вы можете попытаться вызвать метод remove () EntityManager следующим образом:

1
2
3
4
public void removeById(long id) {
    RootEntity rootEntity = entityManager.getReference(RootEntity.class, id);
    entityManager.remove(rootEntity);
}

Прежде всего, мы загружаем эталонное представление сущности, которую мы хотим удалить, и затем передаем эту ссылку в EntityManager. Давайте предположим, что RootEntity сверху имеет дочернее отношение к классу ChildEntity:

1
2
@OneToMany(mappedBy = "rootEntity", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private Set childEntities = new HashSet(0);

Если теперь мы включим свойство show_sql в hibernate, мы будем удивляться, какие выражения SQL выдаются:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
select
        rootentity0_.id as id5_1_,
        rootentity0_.field1 as field2_5_1_,
        rootentity0_.field2 as field3_5_1_,
        childentit1_.PARENT as PARENT5_3_,
        childentit1_.id as id3_,
        childentit1_.id as id4_0_,
        childentit1_.field1 as field2_4_0_,
        childentit1_.field2 as field3_4_0_,
        childentit1_.PARENT as PARENT4_0_
    from
        ROOT_ENTITY rootentity0_
    left outer join
        CHILD_ENTITY childentit1_
            on rootentity0_.id=childentit1_.PARENT
    where
        rootentity0_.id=?
 
    delete
    from
        CHILD_ENTITY
    where
        id=?
 
   delete
   from
       ROOT_ENTITY
   where
       id=?

Почему Hibernate сначала загружает все данные в память, чтобы сразу же удалить эти данные? Причина в том, что жизненный цикл JPA требует, чтобы объект находился в «управляемом» состоянии, прежде чем его можно будет удалить. Только в этом состоянии доступны все функции жизненного цикла, такие как перехватчики (см. Здесь ). Поэтому Hibernate выдает запрос SELECT перед удалением, чтобы перевести и RootEntity, и ChildEntity в «управляемое» состояние. Но что мы можем сделать, если мы просто хотим удалить RootEntity и ChildEntity, если мы знаем идентификатор RootEntity? Ответ заключается в использовании простого запроса DELETE, подобного следующему. Но из-за ограничения целостности дочерней таблицы сначала необходимо удалить все зависимые дочерние объекты. Следующий код демонстрирует, как:

1
2
3
4
5
List childIds = entityManager.createQuery("select c.id from ChildEntity c where c.rootEntity.id = :pid").setParameter("pid", id).getResultList();
for(Long childId : childIds) {
    entityManager.createQuery("delete from ChildEntity c where c.id = :id").setParameter("id", childId).executeUpdate();
}
entityManager.createQuery("delete from RootEntity r where r.id = :id").setParameter("id", id).executeUpdate();

Приведенный выше код приводит к трем операторам SQL, которые мы ожидали бы, вызвав remove (). Теперь вы можете утверждать, что этот способ удаления сложнее, чем просто вызов метода EntityManager remove (). Он также игнорирует аннотации, такие как @OneToMany и @ManyToOne, которые мы поместили в два класса сущностей. Так почему бы не написать некоторый код, который использует знания о двух сущностях, которые уже существуют в двух файлах классов? Прежде всего, мы ищем аннотации @OneToMany, используя отражение в классе RootEntity, извлекаем тип дочернего объекта, а затем ищем его поле обратной связи, аннотированное @ManyToOne. Сделав это, мы можем легко написать три оператора SQL более общим способом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public void delete(EntityManager entityManager, Class parentClass, Object parentId) {
    Field idField = getIdField(parentClass);
    if (idField != null) {
        List oneToManyFields = getOneToManyFields(parentClass);
        for (Field field : oneToManyFields) {
            Class childClass = getFirstActualTypeArgument(field);
            if (childClass != null) {
                Field manyToOneField = getManyToOneField(childClass, parentClass);
                Field childClassIdField = getIdField(childClass);
                if (manyToOneField != null && childClassIdField != null) {
                    List childIds = entityManager.createQuery(String.format("select c.%s from %s c where c.%s.%s = :pid", childClassIdField.getName(), childClass.getSimpleName(), manyToOneField.getName(), idField.getName())).setParameter("pid", parentId).getResultList();
                    for (Long childId : childIds) {
                        entityManager.createQuery(String.format("delete from %s c where c.%s = :id", childClass.getSimpleName(), childClassIdField.getName())).setParameter("id", childId).executeUpdate();
                    }
                }
            }
        }
        entityManager.createQuery(String.format("delete from %s e where e.%s = :id", parentClass.getSimpleName(), idField.getName())).setParameter("id", parentId).executeUpdate();
    }
}

Методы getFirstActualTypeArgument (), getManyToOneField (), getIdField () и getOneToManyFields () в приведенном выше коде здесь не изображены, но делают то, что их название звучит. После внедрения мы можем легко удалить все объекты, начиная с корня дерева.

  • Простой пример приложения, которое можно использовать для изучения поведения и решения, описанного выше, можно найти на github .