Рубрики
Без рубрики

Как выполнить пакетную ВСТАВКУ и ОБНОВЛЕНИЕ инструкций с помощью Hibernate

Автор оригинала: Vlad Mihalcea.

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

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

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

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

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

@OneToMany(
    cascade = CascadeType.ALL, 
    mappedBy = "post", 
    orphanRemoval = true)
private List comments = new ArrayList<>();

Или тестовый сценарий выдает оба ВСТАВИТЬ и ОБНОВИТЕ инструкции, чтобы мы могли проверить, используется ли JDBC пакетирование:

LOGGER.info("Test batch insert");
long startNanos = System.nanoTime();
doInTransaction(session -> {
    int batchSize = batchSize();
    for(int i = 0; i < itemsCount(); i++) {
        if(i % batchSize == 0 && i > 0) {
            session.flush();
            session.clear();
        }

        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);
    }
});

LOGGER.info("{}.testInsert took {} millis",
    getClass().getSimpleName(),
    TimeUnit.NANOSECONDS.toMillis(
        System.nanoTime() - startNanos
    ));

LOGGER.info("Test batch update");
startNanos = System.nanoTime();

doInTransaction(session -> {
    List 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
    ));

Этот тест сохранит настраиваемое количество Записей сущностей, каждая из которых содержит два Комментария . Для краткости мы будем настаивать на 3 Сообщения и Диалект размер пакета по умолчанию:

protected int itemsCount() {
    return 3;
}

protected int batchSize() {
    return Integer.valueOf(Dialect.DEFAULT_BATCH_SIZE);
}

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

Hibernate неявно использует JDBC пакетирование и каждую ВСТАВКУ и ОБНОВЛЕНИЕ инструкция выполняется отдельно:

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 в режиме гибернации (например, рекомендуемые значения от 5 до 30).

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

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

На этот раз Комментарий Инструкции INSERT группируются, в то время как инструкции UPDATE остаются нетронутыми:

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 может сортировать ВСТАВЛЯТЬ и ОБНОВИТЕ инструкции, используя следующие параметры конфигурации:

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

В то время как Сообщение и Комментарий Инструкции INSERT группируются соответствующим образом, инструкции UPDATE по-прежнему выполняются отдельно:

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 конфигурации, которое нам нужно установить, чтобы включить ОБНОВЛЕНИЕ пакетирование:

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

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

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

Теперь оба ВСТАВИТЬ и операторы UPDATE правильно сопоставлены:

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]} 

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

  • в тестовом примере используется база данных PostgreSQL , установленная на одной машине с запущенной в данный момент JVM
  • размер партии 50 был выбран, и каждая тестовая итерация увеличивает количество операторов на порядок
  • все длительности выражаются в миллисекундах

Результаты таковы:

30 191 144 218 178
300 208 217 311 327
3000 556 478 1047 1089
30000 2640 2301 5889 6032
300000 16052 20954 51785 57869

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

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