Статьи

JPA 2.1 критерии удаления / обновления и временных таблиц в Hibernate

Начиная с версии 2.0 JPA EntityManager предлагает метод getCriteriaBuilder() для динамического построения запросов на выборку без необходимости конкатенации строк с использованием языка запросов постоянства Java (JPQL). В версии 2.1 этот CriteriaBuilder предлагает два новых метода createCriteriaDelete() и createCriteriaUpdate() которые позволяют формулировать запросы на удаление и обновление с использованием API критериев.

В целях иллюстрации давайте используем простой пример использования наследования с двумя сущностями Person и Geek :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
@Table(name = "T_PERSON")
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    @Column(name = "FIRST_NAME")
    private String firstName;
    @Column(name = "LAST_NAME")
    private String lastName;
    ...
}
 
@Entity
@Table(name = "T_GEEK")
@Access(AccessType.PROPERTY)
public class Geek extends Person {
    private String favouriteProgrammingLanguage;
    ...
}

Чтобы удалить всех вундеркиндов из нашей базы данных, которые предпочитают Java в качестве языка программирования, мы можем использовать следующий код, используя новый createCriteriaDelete() EntityManager:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
EntityTransaction transaction = null;
try {
    transaction = entityManager.getTransaction();
    transaction.begin();
    CriteriaBuilder builder = entityManager.getCriteriaBuilder();
    CriteriaDelete<Geek> delete = builder.createCriteriaDelete(Geek.class);
    Root<Geek> geekRoot = delete.from(Geek.class);
    delete.where(builder.equal(geekRoot.get("favouriteProgrammingLanguage"), "Java"));
    int numberOfRowsUpdated = entityManager.createQuery(delete).executeUpdate();
    LOGGER.info("Deleted " + numberOfRowsUpdated + " rows.");
    transaction.commit();
} catch (Exception e) {
    if (transaction != null && transaction.isActive()) {
        transaction.rollback();
    }
}

Как и в чистом SQL, мы можем использовать метод from() чтобы указать таблицу, к которой должен быть выполнен запрос на удаление, и where() для объявления наших предикатов. Таким образом, API критериев позволяет определять операции массового удаления динамическим способом, не используя слишком много конкатенаций строк.

Но как выглядит SQL, который создан? Прежде всего, поставщик ORM должен обратить внимание на то, что мы JOINED из иерархии наследования с помощью стратегии JOINED , что означает, что у нас есть две таблицы T_PERSON и T_GEEK где во второй таблице хранится ссылка на родительскую таблицу. Hibernate в версии 4.3.8.Final создает следующие операторы 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
30
31
32
33
34
35
36
37
38
39
40
41
42
insert
into
    HT_T_GEEK
    select
        geek0_.id as id
    from
        T_GEEK geek0_
    inner join
        T_PERSON geek0_1_
            on geek0_.id=geek0_1_.id
    where
        geek0_.FAV_PROG_LANG=?;
 
delete
from
    T_GEEK
where
    (
        id
    ) IN (
        select
            id
        from
            HT_T_GEEK
    );
 
delete
from
    T_PERSON
where
    (
        id
    ) IN (
        select
            id
        from
            HT_T_GEEK
    )
 
delete
from
    HT_T_GEEK;

Как мы видим, Hibernate заполняет временную таблицу идентификаторами гиков / людей, которые соответствуют нашим критериям поиска. Затем он удаляет все строки из таблицы geek, а затем все строки из таблицы person. Наконец временная таблица очищается.

Последовательность операторов удаления ясна, поскольку таблица T_GEEK имеет ограничение внешнего ключа для столбца id таблицы T_PERSON . Следовательно, строки в дочерней таблице должны быть удалены перед строками в родительской таблице. Причина, по которой Hibernate создает временную таблицу, объясняется в этой статье . Подводя итог, можно сказать, что основная проблема заключается в том, что запрос ограничивает строки, которые будут удалены в столбце, который существует только в дочерней таблице. Но строки в дочерней таблице должны быть удалены перед соответствующими строками в родительской таблице. FAV_PROG_LANG='Java' строк в дочерней таблице, то есть всех вундеркиндов с FAV_PROG_LANG='Java' , делает невозможным впоследствии удаление всех соответствующих лиц, так как строки вундеркиндов уже были удалены. Решением этой проблемы является временная таблица, которая сначала собирает все идентификаторы строк, которые должны быть удалены. Когда все идентификаторы известны, эту информацию можно использовать для удаления строк сначала из таблицы geek, а затем из таблицы person.

Сгенерированные операторы SQL выше, конечно, не зависят от использования API критериев. Использование подхода JPQL приводит к тому же сгенерированному SQL:

01
02
03
04
05
06
07
08
09
10
11
12
EntityTransaction transaction = null;
try {
    transaction = entityManager.getTransaction();
    transaction.begin();
    int update = entityManager.createQuery("delete from Geek g where g.favouriteProgrammingLanguage = :lang").setParameter("lang", "Java").executeUpdate();
    LOGGER.info("Deleted " + update + " rows.");
    transaction.commit();
} catch (Exception e) {
    if (transaction != null && transaction.isActive()) {
        transaction.rollback();
    }
}

Когда мы меняем стратегию наследования с JOINED на SINGLE_TABLE , сгенерированные операторы SQL также изменяются на один (здесь столбцом дискриминатора является DTYPE ):

1
2
3
4
5
6
delete
from
    T_PERSON
where
    DTYPE='Geek'
    and FAV_PROG_LANG=?

Вывод

Новые дополнения к API-интерфейсу критериев для удаления и обновления позволяют создавать операторы SQL без необходимости объединения строк. Но имейте в виду, что массовые удаления из иерархии наследования могут заставить базовый ORM использовать временные таблицы для составления списка строк, которые необходимо удалить заранее.