«1. Обзор

В этом руководстве мы рассмотрим, как можно пакетно вставлять или обновлять объекты с помощью Hibernate/JPA.

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

2. Настройка

2.1. Пример модели данных

Давайте посмотрим на нашу модель данных, которую мы будем использовать в примерах.

Во-первых, мы создадим сущность школы:

@Entity
public class School {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String name;

    @OneToMany(mappedBy = "school")
    private List<Student> students;

    // Getters and setters...
}

В каждой школе будет ноль или более учеников:

@Entity
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String name;

    @ManyToOne
    private School school;

    // Getters and setters...
}

2.2. Отслеживание SQL-запросов

При выполнении наших примеров нам нужно убедиться, что операторы вставки/обновления действительно отправляются пакетами. К сожалению, из операторов журнала Hibernate мы не можем понять, являются ли операторы SQL пакетными или нет. По этой причине мы будем использовать прокси-сервер источника данных для трассировки SQL-операторов Hibernate/JPA:

private static class ProxyDataSourceInterceptor implements MethodInterceptor {
    private final DataSource dataSource;
    public ProxyDataSourceInterceptor(final DataSource dataSource) {
        this.dataSource = ProxyDataSourceBuilder.create(dataSource)
            .name("Batch-Insert-Logger")
            .asJson().countQuery().logQueryToSysOut().build();
    }
    
    // Other methods...
}

3. Поведение по умолчанию

По умолчанию Hibernate не включает пакетную обработку. Это означает, что для каждой операции вставки/обновления будет отправлен отдельный оператор SQL:

@Transactional
@Test
public void whenNotConfigured_ThenSendsInsertsSeparately() {
    for (int i = 0; i < 10; i++) {
        School school = createSchool(i);
        entityManager.persist(school);
    }
    entityManager.flush();
}

Здесь мы сохранили 10 сущностей School. Если мы посмотрим на журналы запросов, мы увидим, что Hibernate отправляет каждый оператор вставки отдельно:

"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School1","1"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School2","2"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School3","3"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School4","4"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School5","5"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School6","6"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School7","7"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School8","8"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School9","9"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School10","10"]]

Следовательно, мы должны настроить Hibernate для включения пакетной обработки. Для этого мы должны установить свойство hibernate.jdbc.batch_size на число больше 0.

Если мы создаем EntityManager вручную, мы должны добавить hibernate.jdbc.batch_size в свойства Hibernate:

public Properties hibernateProperties() {
    Properties properties = new Properties();
    properties.put("hibernate.jdbc.batch_size", "5");
    
    // Other properties...
    return properties;
}

Если мы используем Spring Boot, мы можем определить его как свойство приложения:

spring.jpa.properties.hibernate.jdbc.batch_size=5

4. Пакетная вставка для одной таблицы

4.1. Пакетная вставка без явной очистки

Давайте сначала посмотрим, как можно использовать пакетную вставку, когда мы имеем дело только с одним типом сущности.

Мы будем использовать предыдущий пример кода, но на этот раз пакетная обработка включена:

@Transactional
@Test
public void whenInsertingSingleTypeOfEntity_thenCreatesSingleBatch() {
    for (int i = 0; i < 10; i++) {
        School school = createSchool(i);
        entityManager.persist(school);
    }
}

Здесь мы сохранили 10 объектов School. Когда мы смотрим на журналы, мы можем убедиться, что Hibernate отправляет операторы вставки пакетами:

"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School1","1"],["School2","2"],["School3","3"],["School4","4"],["School5","5"]]
"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School6","6"],["School7","7"],["School8","8"],["School9","9"],["School10","10"]]

Одна важная вещь, которую следует упомянуть, это потребление памяти. Когда мы сохраняем объект, Hibernate сохраняет его в контексте сохранения. Например, если мы сохраняем 100 000 сущностей в одной транзакции, в итоге у нас будет 100 000 экземпляров сущностей в памяти, что может вызвать исключение OutOfMemoryException.

4.2. Пакетная вставка с явным сбросом

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

Прежде всего, контекст персистентности хранит в памяти вновь созданные объекты, а также измененные. Hibernate отправляет эти изменения в базу данных при синхронизации транзакции. Обычно это происходит в конце транзакции. Однако вызов EntityManager.flush() также запускает синхронизацию транзакций.

Во-вторых, контекст персистентности служит кешем сущностей, поэтому его также называют кешем первого уровня. Чтобы очистить объекты в контексте персистентности, мы можем вызвать EntityManager.clear().

Итак, чтобы уменьшить нагрузку на память во время пакетной обработки, мы можем вызывать EntityManager.flush() и EntityManager.clear() в коде нашего приложения всякий раз, когда достигается размер пакета:

@Transactional
@Test
public void whenFlushingAfterBatch_ThenClearsMemory() {
    for (int i = 0; i < 10; i++) {
        if (i > 0 && i % BATCH_SIZE == 0) {
            entityManager.flush();
            entityManager.clear();
        }
        School school = createSchool(i);
        entityManager.persist(school);
    }
}

Здесь мы очищаем сущности в контексте персистентности, что заставляет Hibernate отправлять запросы в базу данных. Кроме того, очищая контекст сохраняемости, мы удаляем объекты School из памяти. Поведение при пакетировании останется прежним.

5. Пакетная вставка для нескольких таблиц

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

Когда мы хотим сохранить сущности нескольких типов, Hibernate создает разные пакеты для каждого типа сущности. Это связано с тем, что в одном пакете может быть только один тип объектов.

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

@Transactional
@Test
public void whenThereAreMultipleEntities_ThenCreatesNewBatch() {
    for (int i = 0; i < 10; i++) {
        if (i > 0 && i % BATCH_SIZE == 0) {
            entityManager.flush();
            entityManager.clear();
        }
        School school = createSchool(i);
        entityManager.persist(school);
        Student firstStudent = createStudent(school);
        Student secondStudent = createStudent(school);
        entityManager.persist(firstStudent);
        entityManager.persist(secondStudent);
    }
}

«

«Здесь мы вставляем школу, назначаем двух учеников и повторяем этот процесс 10 раз.

"batch":true, "querySize":1, "batchSize":1, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School1","1"]]
"batch":true, "querySize":1, "batchSize":2, "query":["insert into student (name, school_id, id) 
  values (?, ?, ?)"], "params":[["Student-School1","1","2"],["Student-School1","1","3"]]
"batch":true, "querySize":1, "batchSize":1, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School2","4"]]
"batch":true, "querySize":1, "batchSize":2, "query":["insert into student (name, school_id, id) 
  values (?, ?, ?)"], "params":[["Student-School2","4","5"],["Student-School2","4","6"]]
"batch":true, "querySize":1, "batchSize":1, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School3","7"]]
"batch":true, "querySize":1, "batchSize":2, "query":["insert into student (name, school_id, id) 
  values (?, ?, ?)"], "params":[["Student-School3","7","8"],["Student-School3","7","9"]]
Other log lines...

В журналах мы видим, что Hibernate отправляет операторы вставки School несколькими пакетами размера 1, в то время как мы ожидали только 2 пакета размера 5. Более того, операторы вставки Student также отправляются несколькими пакетами размера 2 вместо 4 пакетов. размера 5:

Чтобы объединить все операторы вставки одного и того же типа объекта, мы должны настроить свойство hibernate.order_inserts.

public Properties hibernateProperties() {
    Properties properties = new Properties();
    properties.put("hibernate.order_inserts", "true");
    
    // Other properties...
    return properties;
}

Мы можем настроить свойство Hibernate вручную с помощью EntityManagerFactory:

spring.jpa.properties.hibernate.order_inserts=true

Если мы используем Spring Boot, мы можем настроить свойство в application.properties:

"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School6","16"],["School7","19"],["School8","22"],["School9","25"],["School10","28"]]
"batch":true, "querySize":1, "batchSize":5, "query":["insert into student (name, school_id, id) 
  values (?, ?, ?)"], "params":[["Student-School6","16","17"],["Student-School6","16","18"],
  ["Student-School7","19","20"],["Student-School7","19","21"],["Student-School8","22","23"]]
"batch":true, "querySize":1, "batchSize":5, "query":["insert into student (name, school_id, id) 
  values (?, ?, ?)"], "params":[["Student-School8","22","24"],["Student-School9","25","26"],
  ["Student-School9","25","27"],["Student-School10","28","29"],["Student-School10","28","30"]]

После добавления этого свойства мы У нас будет 1 пакет для школьных вставок и 2 пакета для студенческих вставок:

6. Пакетное обновление

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

Чтобы включить это, мы настроим свойства hibernate.order_updates и hibernate.jdbc.batch_versioned_data.

public Properties hibernateProperties() {
    Properties properties = new Properties();
    properties.put("hibernate.order_updates", "true");
    properties.put("hibernate.batch_versioned_data", "true");
    
    // Other properties...
    return properties;
}

Если мы создаем нашу EntityManagerFactory вручную, мы можем установить свойства программно:

spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.batch_versioned_data=true

А если мы используем Spring Boot, мы просто добавим их в application.properties:

@Transactional
@Test
public void whenUpdatingEntities_thenCreatesBatch() {
    TypedQuery<School> schoolQuery = 
      entityManager.createQuery("SELECT s from School s", School.class);
    List<School> allSchools = schoolQuery.getResultList();
    for (School school : allSchools) {
        school.setName("Updated_" + school.getName());
    }
}

~~ ~ После настройки этих свойств Hibernate должен сгруппировать операторы обновления в пакеты:

"batch":true, "querySize":1, "batchSize":5, "query":["update school set name=? where id=?"], 
  "params":[["Updated_School1","1"],["Updated_School2","2"],["Updated_School3","3"],
  ["Updated_School4","4"],["Updated_School5","5"]]
"batch":true, "querySize":1, "batchSize":5, "query":["update school set name=? where id=?"], 
  "params":[["Updated_School6","6"],["Updated_School7","7"],["Updated_School8","8"],
  ["Updated_School9","9"],["Updated_School10","10"]]

Здесь мы обновили школьные объекты, и Hibernate отправляет операторы SQL двумя пакетами размером 5:

7. Генерация @Id Стратегия

Когда мы хотим использовать пакетную обработку для вставок/обновлений, мы должны знать о стратегии генерации первичного ключа. Если наши объекты используют генератор идентификаторов GenerationType.IDENTITY, Hibernate автоматически отключит пакетные вставки/обновления.

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;

Поскольку объекты в наших примерах используют генератор идентификаторов GenerationType.SEQUENCE, Hibernate позволяет выполнять пакетные операции:

8. Резюме

В этой статье мы рассмотрели пакетные вставки и обновления с использованием Hibernate/JPA.