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<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 школьных
образований. Если мы посмотрим на журналы запросов, то увидим, что 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 школьных
образований. Когда мы смотрим на журналы, мы можем убедиться, что 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);
}
}
Здесь мы вставляем School,
назначаем ей два Student
и повторяем этот процесс 10 раз.
В логах мы видим, что Hibernate отправляет операторы вставки School
несколькими пакетами размера 1, в то время как мы ожидали только 2 пакета размера 5. Более того, операторы вставки Student
также отправляются несколькими пакетами размера 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...
Чтобы объединить все операторы вставки одного и того же типа сущности, мы должны настроить свойство 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 пакет для вставок School и 2 пакета для вкладышей
Student
:
"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.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, мы просто добавим их в application.properties:
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.batch_versioned_data=true
После настройки этих свойств Hibernate должен сгруппировать операторы обновления в пакеты:
@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 отправляет операторы SQL двумя пакетами размером 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.SEQUENCE , Hibernate позволяет выполнять пакетные операции:
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
8. Резюме
В этой статье мы рассмотрели пакетные вставки и обновления с использованием Hibernate/JPA.
Ознакомьтесь с примерами кода для этой статьи на Github .