Статьи

Как пакетировать операторы DELETE с помощью Hibernate

Вступление

В моем предыдущем посте я объяснил конфигурации Hibernate, необходимые для пакетной обработки операторов INSERT и UPDATE. Этот пост продолжит эту тему с пакетным оператором DELETE.

Объекты модели домена

Начнем со следующей модели сущности:

postcommentdetailsbatchdelete

Сущность Post имеет связь « один ко многим» с комментарием и отношение « один к одному» с сущностью PostDetails :

1
2
3
4
5
6
7
@OneToMany(cascade = CascadeType.ALL, mappedBy = "post",
        orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
 
@OneToOne(cascade = CascadeType.ALL, mappedBy = "post",
        orphanRemoval = true, fetch = FetchType.LAZY)
private PostDetails details;

Предстоящие тесты будут выполняться на следующих данных:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
doInTransaction(session -> {
    int batchSize = batchSize();
    for(int i = 0; i < itemsCount(); i++) {
        int j = 0;
 
        Post post = new Post(String.format(
            "Post no. %d", i));       
        post.addComment(new Comment( String.format(
            "Post comment %d:%d", i, j++)));
        post.addComment(new Comment(String.format(
            "Post comment %d:%d", i, j++)));
        post.addDetails(new PostDetails());
 
        session.persist(post);
        if(i % batchSize == 0 && i > 0) {
            session.flush();
            session.clear();
        }
    }
});

Конфигурация гибернации

Как уже объяснялось , для пакетных операторов INSERT и UPDATE необходимы следующие свойства:

1
2
3
4
5
6
7
8
properties.put("hibernate.jdbc.batch_size",
    String.valueOf(batchSize()));
properties.put("hibernate.order_inserts",
    "true");
properties.put("hibernate.order_updates",
    "true");
properties.put("hibernate.jdbc.batch_versioned_data",
    "true");

Далее мы собираемся проверить, упакованы ли операторы DELETE.

JPA Каскад Удалить

Поскольку каскадные переходы состояний сущностей удобны, я собираюсь доказать, что пакетная обработка CascadeType.DELETE и JDBC плохо сочетается .

Следующие тесты собираются:

  • Выберите несколько сообщений вместе с комментариями и сообщениями
  • Удалите сообщения , одновременно распространяя событие удаления на Комментарии и PostDetails.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testCascadeDelete() {
    LOGGER.info("Test batch delete with cascade");
    final AtomicReference<Long> startNanos =
        new AtomicReference<>();
    addDeleteBatchingRows();
    doInTransaction(session -> {
        List<Post> posts = session.createQuery(
            "select distinct p " +
            "from Post p " +
            "join fetch p.details d " +
            "join fetch p.comments c")
        .list();
        startNanos.set(System.nanoTime());
        for (Post post : posts) {
            session.delete(post);
        }
    });
    LOGGER.info("{}.testCascadeDelete took {} millis",
        getClass().getSimpleName(),
        TimeUnit.NANOSECONDS.toMillis(
            System.nanoTime() - startNanos.get()
    ));
}

Запуск этого теста дает следующий вывод:

1
2
3
4
5
6
7
8
9
Query:{[delete from Comment where id=? and version=?][55,0]} {[delete from Comment where id=? and version=?][56,0]}
Query:{[delete from PostDetails where id=?][3]}
Query:{[delete from Post where id=? and version=?][3,0]}
Query:{[delete from Comment where id=? and version=?][54,0]} {[delete from Comment where id=? and version=?][53,0]}
Query:{[delete from PostDetails where id=?][2]}
Query:{[delete from Post where id=? and version=?][2,0]}
Query:{[delete from Comment where id=? and version=?][52,0]} {[delete from Comment where id=? and version=?][51,0]}
Query:{[delete from PostDetails where id=?][1]}
Query:{[delete from Post where id=? and version=?][1,0]}

Были объединены только операторы Comment DELETE, остальные объекты были удалены в отдельных обходах базы данных.

Причину такого поведения дает реализация сортировки ActionQueue :

1
2
3
4
5
6
7
if ( session.getFactory().getSettings().isOrderUpdatesEnabled() ) {
    // sort the updates by pk
    updates.sort();
}
if ( session.getFactory().getSettings().isOrderInsertsEnabled() ) {
    insertions.sort();
}

В то время как INSERTS и UPDATES включены, операторы DELETE не сортируются вообще. Пакет JDBC можно использовать повторно только тогда, когда все операторы принадлежат одной и той же таблице базы данных. Когда входящий оператор нацелен на другую таблицу базы данных, текущий пакет должен быть освобожден, чтобы новый пакет соответствовал текущей таблице базы данных операторов:

01
02
03
04
05
06
07
08
09
10
11
12
13
public Batch getBatch(BatchKey key) {
    if ( currentBatch != null ) {
        if ( currentBatch.getKey().equals( key ) ) {
            return currentBatch;
        }
        else {
            currentBatch.execute();
            currentBatch.release();
        }
    }
    currentBatch = batchBuilder().buildBatch(key, this);
    return currentBatch;
}

Удаление сирот и ручная промывка

Обходной путь — отсоединить все дочерние сущности при ручной очистке сеанса Hibernate перед переходом к новой дочерней ассоциации:

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
@Test
public void testOrphanRemoval() {
    LOGGER.info("Test batch delete with orphan removal");
    final AtomicReference<Long> startNanos =
        new AtomicReference<>();
 
    addDeleteBatchingRows();
 
    doInTransaction(session -> {
        List<Post> posts = session.createQuery(
            "select distinct p " +
            "from Post p " +
            "join fetch p.details d " +
            "join fetch p.comments c")
        .list();
 
        startNanos.set(System.nanoTime());
 
        posts.forEach(Post::removeDetails);
        session.flush();
 
        posts.forEach(post -> {
            for (Iterator<Comment> commentIterator =
                     post.getComments().iterator();
                     commentIterator.hasNext(); ) {
                Comment comment =  commentIterator.next();
                comment.post = null;
                commentIterator.remove();
            }
        });
        session.flush();
 
        posts.forEach(session::delete);
    });
    LOGGER.info("{}.testOrphanRemoval took {} millis",
        getClass().getSimpleName(),
        TimeUnit.NANOSECONDS.toMillis(
            System.nanoTime() - startNanos.get()
    ));
}

На этот раз все операторы DELETE правильно упакованы:

1
2
3
Query:{[delete from PostDetails where id=?][2]} {[delete from PostDetails where id=?][3]} {[delete from PostDetails where id=?][1]}
Query:{[delete from Comment where id=? and version=?][53,0]} {[delete from Comment where id=? and version=?][54,0]} {[delete from Comment where id=? and version=?][56,0]} {[delete from Comment where id=? and version=?][55,0]} {[delete from Comment where id=? and version=?][52,0]} {[delete from Comment where id=? and version=?][51,
Query:{[delete from Post where id=? and version=?][2,0]} {[delete from Post where id=? and version=?][3,0]} {[delete from Post where id=? and version=?][1,0]}

SQL Каскад Удалить

Лучшим решением является использование каскадного удаления SQL вместо механизма распространения состояния сущности JPA . Таким образом, мы также можем уменьшить количество операторов DML . Поскольку Hibernate Session действует как кэш транзакционной записи с обратной записью , мы должны быть особенно осторожны при смешивании переходов состояния сущности с автоматическими действиями на стороне базы данных, поскольку контекст постоянства может не отражать последние изменения базы данных.

Ассоциация Комментария « один-ко-многим» сущности Post помечена специфичной для Hibernate аннотацией @OnDelete , поэтому автоматически генерируемая схема базы данных включает в себя директиву ON DELETE CASCADE :

1
2
3
4
5
6
@OneToMany(cascade = {
       CascadeType.PERSIST,
       CascadeType.MERGE},
    mappedBy = "post")
@OnDelete(action = OnDeleteAction.CASCADE)
private List<Comment> comments = new ArrayList<>();

Генерация следующего DDL :

1
2
3
alter table Comment add constraint
FK_apirq8ka64iidc18f3k6x5tc5 foreign key (post_id)
references Post on delete cascade

То же самое делается с непосредственным сопоставлением сущностей PostDetails :

1
2
3
4
5
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id")
@MapsId
@OnDelete(action = OnDeleteAction.CASCADE)
private Post post;

И связанный DDL :

1
2
3
alter table PostDetails add constraint
FK_h14un5v94coafqonc6medfpv8 foreign key (id)
references Post on delete cascade

CascadeType.ALL и orphanRemoval были заменены на CascadeType.PERSIST и CascadeType.MERGE , потому что мы больше не хотим, чтобы Hibernate распространял событие удаления сущности.

Тест удаляет только объекты Post.

1
2
3
4
5
6
7
8
9
doInTransaction(session -> {
    List<Post> posts = session.createQuery(
        "select p from Post p")
    .list();
    startNanos.set(System.nanoTime());
    for (Post post : posts) {
        session.delete(post);
    }
});

Операторы DELETE правильно упакованы, поскольку существует только одна таблица назначения.

1
Query:{[delete from Post where id=? and version=?][1,0]} {[delete from Post where id=? and version=?][2,0]} {[delete from Post where id=? and version=?][3,0]}

Вывод

Если пакетирование операторов INSERT и UPDATE является лишь вопросом конфигурации, операторы DELETE требуют некоторых дополнительных шагов, которые могут увеличить сложность уровня доступа к данным.

  • Код доступен на GitHub .