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

Пакетная вставка/обновление с помощью Hibernate/JPA

Узнайте, как использовать пакетные вставки и обновления с помощью Hibernate/JPA

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

1. Обзор

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

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

2. Настройка

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

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

Во-первых, мы создадим объект School :

@Entity
public class School {

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

    private String name;

    @OneToMany(mappedBy = "school")
    private List 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-запросов

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

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. Поведение по умолчанию

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

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

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

"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.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 Школьных сущностей. Просматривая журналы, мы можем убедиться, что Hibernate отправляет инструкции insert пакетами:

"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 собирает инструкции insert, всякий раз, когда он сталкивается с типом сущности, отличным от типа в текущем пакете, он создает новый пакет. Это имеет место, даже если для этого типа сущностей уже существует пакет:

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

Здесь мы вставляем School и назначаем ему два Student s и повторяем этот процесс 10 раз.

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

"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...

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

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

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

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

spring.jpa.properties.hibernate.order_inserts=true

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

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

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

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

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

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

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

И если мы используем Spring Boot, мы просто добавим их в приложение.свойства:

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

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

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

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

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

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

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

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

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

8. Резюме

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

Проверьте примеры кода для этой статьи на Github .