Статьи

Как пакетировать операторы INSERT и UPDATE с Hibernate

Вступление

JDBC уже давно предлагает поддержку пакетной обработки операторов DML . По умолчанию все операторы отправляются один за другим, каждый в отдельном обходе сети. Пакетная обработка позволяет нам отправлять несколько операторов за один раз, сохраняя ненужную очистку потока сокетов.

Hibernate скрывает операторы базы данных за уровнем абстракции транзакционной записи . Промежуточный уровень позволяет нам скрыть семантику пакетирования JDBC от логики постоянного уровня. Таким образом, мы можем изменить стратегию пакетирования JDBC без изменения кода доступа к данным.

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

Время тестирования

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

postcommentjdbcbatch

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

1
2
3
4
5
@OneToMany(
    cascade = CascadeType.ALL,
    mappedBy = "post",
    orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();

Или тестовый сценарий выдает операторы INSERT и UPDATE , поэтому мы можем проверить, используется ли пакетная обработка JDBC :

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
43
44
45
46
47
48
49
50
51
52
53
LOGGER.info("Test batch insert");
long startNanos = System.nanoTime();
doInTransaction(session -> {
    int batchSize = batchSize();
    for(int i = 0; i < itemsCount(); i++) {
        Post post = new Post(
            String.format("Post no. %d", i)
        );
        int j = 0;
        post.addComment(new Comment(
                String.format(
                    "Post comment %d:%d", i, j++
        )));
        post.addComment(new Comment(
                String.format(
                     "Post comment %d:%d", i, j++
        )));
        session.persist(post);
        if(i % batchSize == 0 && i > 0) {
            session.flush();
            session.clear();
        }
    }
});
LOGGER.info("{}.testInsert took {} millis",
    getClass().getSimpleName(),
    TimeUnit.NANOSECONDS.toMillis(
        System.nanoTime() - startNanos
    ));
 
LOGGER.info("Test batch update");
startNanos = System.nanoTime();
 
doInTransaction(session -> {
    List<Post> posts = session.createQuery(
        "select distinct p " +
        "from Post p " +
        "join fetch p.comments c")
    .list();
 
    for(Post post : posts) {
        post.title = "Blog " + post.title;
        for(Comment comment : post.comments) {
            comment.review = "Blog " + comment.review;
        }
    }
});
 
LOGGER.info("{}.testUpdate took {} millis",
    getClass().getSimpleName(),
    TimeUnit.NANOSECONDS.toMillis(
        System.nanoTime() - startNanos
    ));

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

1
2
3
4
5
6
7
protected int itemsCount() {
    return 3;
}
 
protected int batchSize() {
    return Integer.valueOf(Dialect.DEFAULT_BATCH_SIZE);
}

Пакетная поддержка по умолчанию

Hibernate неявно использует пакетную обработку JDBC, и каждый оператор INSERT и UPDATE выполняется отдельно:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
Query:{[insert into Post (title, version, id) values (?, ?, ?)][Post no. 0,0,1]}
Query:{[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][1,Post comment 0:0,0,51]}
Query:{[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][1,Post comment 0:1,0,52]}
Query:{[insert into Post (title, version, id) values (?, ?, ?)][Post no. 1,0,2]}
Query:{[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][2,Post comment 1:0,0,53]}
Query:{[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][2,Post comment 1:1,0,54]}
Query:{[insert into Post (title, version, id) values (?, ?, ?)][Post no. 2,0,3]}
Query:{[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][3,Post comment 2:0,0,55]}
Query:{[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][3,Post comment 2:1,0,56]}
 
Query:{[update Post set title=?, version=? where id=? and version=?][Blog Post no. 1,1,2,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][2,Blog Post comment 1:0,1,53,0]}
Query:{[update Post set title=?, version=? where id=? and version=?][Blog Post no. 0,1,1,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][1,Blog Post comment 0:1,1,52,0]}
Query:{[update Post set title=?, version=? where id=? and version=?][Blog Post no. 2,1,3,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][3,Blog Post comment 2:0,1,55,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][3,Blog Post comment 2:1,1,56,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][1,Blog Post comment 0:0,1,51,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][2,Blog Post comment 1:1,1,54,0]}

Настройка hibernate.jdbc.batch_size

Чтобы включить пакетную обработку JDBC , нам нужно настроить свойство hibernate.jdbc.batch_size :

Ненулевое значение позволяет использовать пакетные обновления JDBC2 в Hibernate (например, рекомендуемые значения от 5 до 30)

Мы установим это свойство и повторно запустим наш тест:

1
2
properties.put("hibernate.jdbc.batch_size",
    String.valueOf(batchSize()));

На этот раз операторы Comment INSERT объединяются, а операторы UPDATE остаются нетронутыми:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
Query:{[insert into Post (title, version, id) values (?, ?, ?)][Post no. 0,0,1]}
Query:{[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][1,Post comment 0:0,0,51]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][1,Post comment 0:1,0,52]}
Query:{[insert into Post (title, version, id) values (?, ?, ?)][Post no. 1,0,2]}
Query:{[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][2,Post comment 1:0,0,53]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][2,Post comment 1:1,0,54]}
Query:{[insert into Post (title, version, id) values (?, ?, ?)][Post no. 2,0,3]}
Query:{[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][3,Post comment 2:0,0,55]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][3,Post comment 2:1,0,56]}
 
Query:{[update Post set title=?, version=? where id=? and version=?][Blog Post no. 1,1,2,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][2,Blog Post comment 1:0,1,53,0]}
Query:{[update Post set title=?, version=? where id=? and version=?][Blog Post no. 0,1,1,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][1,Blog Post comment 0:1,1,52,0]}
Query:{[update Post set title=?, version=? where id=? and version=?][Blog Post no. 2,1,3,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][3,Blog Post comment 2:0,1,55,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][3,Blog Post comment 2:1,1,56,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][1,Blog Post comment 0:0,1,51,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][2,Blog Post comment 1:1,1,54,0]}

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

Заявления о заказе

Hibernate может сортировать операторы INSERT и UPDATE, используя следующие параметры конфигурации:

1
2
properties.put("hibernate.order_inserts", "true");
properties.put("hibernate.order_updates", "true");

В то время как операторы Post и Comment INSERT упакованы соответствующим образом, операторы UPDATE по-прежнему выполняются отдельно:

01
02
03
04
05
06
07
08
09
10
11
12
Query:{[insert into Post (title, version, id) values (?, ?, ?)][Post no. 0,0,1]} {[insert into Post (title, version, id) values (?, ?, ?)][Post no. 1,0,2]} {[insert into Post (title, version, id) values (?, ?, ?)][Post no. 2,0,3]}
Query:{[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][1,Post comment 0:0,0,51]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][1,Post comment 0:1,0,52]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][2,Post comment 1:0,0,53]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][2,Post comment 1:1,0,54]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][3,Post comment 2:0,0,55]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][3,Post comment 2:1,0,56]}
 
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][1,Blog Post comment 0:0,1,51,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][1,Blog Post comment 0:1,1,52,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][2,Blog Post comment 1:0,1,53,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][2,Blog Post comment 1:1,1,54,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][3,Blog Post comment 2:0,1,55,0]}
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][3,Blog Post comment 2:1,1,56,0]}
Query:{[update Post set title=?, version=? where id=? and version=?][Blog Post no. 0,1,1,0]}
Query:{[update Post set title=?, version=? where id=? and version=?][Blog Post no. 1,1,2,0]}
Query:{[update Post set title=?, version=? where id=? and version=?][Blog Post no. 2,1,3,0]}

Добавление версии пакета данных поддержки

Нам нужно установить свойство конфигурации hibernate.jdbc.batch_versioned_data , чтобы включить пакетную обработку UPDATE :

Установите для этого свойства значение true, если ваш драйвер JDBC возвращает правильное количество строк из executeBatch (). Обычно безопасно включить эту опцию. Затем Hibernate будет использовать пакетный DML для автоматически версионных данных. По умолчанию false.

Мы перезапустим наш тест с этим набором свойств:

1
properties.put("hibernate.jdbc.batch_versioned_data", "true");

Теперь операторы INSERT и UPDATE правильно упакованы:

1
2
3
4
5
Query:{[insert into Post (title, version, id) values (?, ?, ?)][Post no. 0,0,1]} {[insert into Post (title, version, id) values (?, ?, ?)][Post no. 1,0,2]} {[insert into Post (title, version, id) values (?, ?, ?)][Post no. 2,0,3]}
Query:{[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][1,Post comment 0:0,0,51]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][1,Post comment 0:1,0,52]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][2,Post comment 1:0,0,53]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][2,Post comment 1:1,0,54]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][3,Post comment 2:0,0,55]} {[insert into Comment (post_id, review, version, id) values (?, ?, ?, ?)][3,Post comment 2:1,0,56]}
 
Query:{[update Comment set post_id=?, review=?, version=? where id=? and version=?][1,Blog Post comment 0:0,1,51,0]} {[update Comment set post_id=?, review=?, version=? where id=? and version=?][1,Blog Post comment 0:1,1,52,0]} {[update Comment set post_id=?, review=?, version=? where id=? and version=?][2,Blog Post comment 1:0,1,53,0]} {[update Comment set post_id=?, review=?, version=? where id=? and version=?][2,Blog Post comment 1:1,1,54,0]} {[update Comment set post_id=?, review=?, version=? where id=? and version=?][3,Blog Post comment 2:0,1,55,0]} {[update Comment set post_id=?, review=?, version=? where id=? and version=?][3,Blog Post comment 2:1,1,56,0]}
Query:{[update Post set title=?, version=? where id=? and version=?][Blog Post no. 0,1,1,0]} {[update Post set title=?, version=? where id=? and version=?][Blog Post no. 1,1,2,0]} {[update Post set title=?, version=? where id=? and version=?][Blog Post no. 2,1,3,0]}

эталонный тест

Теперь, когда нам удалось настроить Hibernate для пакетной обработки JDBC , мы можем оценить прирост производительности группировки операторов.

  • В тестовом примере используется база данных PostgreSQL, установленная на той же машине, где в настоящий момент работает JVM
  • был выбран размер партии 50, и каждая итерация теста на порядок увеличивает количество операторов
  • все длительности выражены в миллисекундах
Количество выписок Нет партии Вставить продолжительность Нет пакета Продолжительность обновления Продолжительность пакетной вставки Продолжительность пакетного обновления
30 218 178 191 144
300 311 327 208 217
3000 1047 1089 556 478
30000 5889 6032 2640 2301
300000 51785 57869 16052 20954

Чем больше строк мы ВСТАВЛЯЕМ или ОБНОВЛЯЕМ , тем больше мы можем извлечь выгоду из пакетной обработки JDBC . Для большинства приложений записи (например, корпоративных корпоративных процессоров ) нам определенно следует включить пакетную обработку JDBC, поскольку преимущества в производительности могут быть ошеломляющими .

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