Inserção / atualização em lote com Hibernate / JPA
1. Visão geral
Neste tutorial, veremos como podemos inserir em lote ou atualizar entidades usandoHibernate/JPA.
Os lotes nos permitem enviar um grupo de instruções SQL para o banco de dados em uma única chamada de rede. Dessa forma, podemos otimizar o uso de rede e memória de nosso aplicativo.
2. Configuração
2.1. Modelo de Dados de Amostra
Vejamos nosso modelo de dados de amostra que usaremos nos exemplos.
Em primeiro lugar, criaremos uma entidadeSchool:
@Entity
public class School {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
@OneToMany(mappedBy = "school")
private List students;
// Getters and setters...
}
CadaSchool terá zero ou maisStudents:
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
@ManyToOne
private School school;
// Getters and setters...
}
2.2. Rastreando consultas SQL
Ao executar nossos exemplos, precisaremos verificar se as instruções de inserção / atualização são realmente enviadas em lotes. Infelizmente, não podemos entender porHibernate log statements se as instruções SQL são agrupadas ou não. Por causa disso, usaremos um proxy de fonte de dados para rastrear instruções 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. Comportamento padrão
Hibernate doesn’t enable batching by default. Isso significa que ele enviará uma instrução SQL separada para cada operação de inserção / atualização:
@Transactional
@Test
public void whenNotConfigured_ThenSendsInsertsSeparately() {
for (int i = 0; i < 10; i++) {
School school = createSchool(i);
entityManager.persist(school);
}
entityManager.flush();
}
Aqui, persistimos entidades de 10School. Se olharmos para os logs de consulta, podemos ver que o Hibernate envia cada instrução de inserção separadamente:
"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"]]
Portanto, devemos configurar o Hibernate para ativar o lote. Para isso,we should set hibernate.jdbc.batch_size property to a number bigger than 0.
Se estivermos criandoEntityManager manualmente, devemos adicionarhibernate.jdbc.batch_size às propriedades do Hibernate:
public Properties hibernateProperties() {
Properties properties = new Properties();
properties.put("hibernate.jdbc.batch_size", "5");
// Other properties...
return properties;
}
Se estivermos usando Spring Boot, podemos defini-lo como uma propriedade do aplicativo:
spring.jpa.properties.hibernate.jdbc.batch_size=5
4. Inserção em lote para tabela única
4.1. Inserção em lote sem liberação explícita
Vejamos primeiro como podemos usar inserções em lote quando estamos lidando com apenas um tipo de entidade.
Usaremos o exemplo de código anterior, mas desta vez o lote está habilitado:
@Transactional
@Test
public void whenInsertingSingleTypeOfEntity_thenCreatesSingleBatch() {
for (int i = 0; i < 10; i++) {
School school = createSchool(i);
entityManager.persist(school);
}
}
Aqui, persistimos entidades de 10School. Quando olhamos para os logs, podemos verificar se o Hibernate envia instruções de inserção em lotes:
"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"]]
Uma coisa importante a mencionar aqui é o consumo de memória. When we persist an entity, Hibernate stores it in the persistence context. Por exemplo, se persistirmos 100.000 entidades em uma transação, acabaremos tendo 100.000 instâncias de entidade na memória, possivelmente causando umOutOfMemoryException.
4.2. Inserção em lote com liberação explícita
Agora, veremos como podemos otimizar o uso de memória durante as operações em lote. Vamos nos aprofundar na função do contexto de persistência.
Primeiro, o contexto de persistência armazena entidades recém-criadas e também as modificadas na memória. O Hibernate envia essas alterações para o banco de dados quando a transação é sincronizada. Isso geralmente acontece no final de uma transação. No entanto,calling EntityManager.flush() also triggers a transaction synchronization.
Em segundo lugar, o contexto de persistência serve como um cache de entidade, também chamado de cache de primeiro nível. To clear entities in the persistence context, we can call*EntityManager.clear()*.
Portanto, para reduzir a carga de memória durante o envio em lote, podemos chamarEntityManager.flush()eEntityManager.clear() no código do nosso aplicativo, sempre que o tamanho do lote for atingido:
@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);
}
}
Aqui, estamos liberando as entidades no contexto de persistência, fazendo com que o Hibernate envie consultas ao banco de dados. Além disso, ao limpar o contexto de persistência, estamos removendo as entidadesSchool da memória. O comportamento dos lotes permanecerá o mesmo.
5. Inserção em lote para várias tabelas
Agora vamos ver como podemos configurar inserções em lote ao lidar com vários tipos de entidade em uma transação.
Quando queremos persistir as entidades de vários tipos, o Hibernate cria um lote diferente para cada tipo de entidade. Isso ocorre porquethere can be only one type of entity in a single batch.
Além disso, como o Hibernate coleta instruções de inserção, sempre que encontra um tipo de entidade diferente daquele no lote atual, ele cria um novo lote. Este é o caso, embora já exista um lote para esse tipo de entidade:
@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);
}
}
Aqui, estamos inserindo umSchoole atribuindo doisStudents e repetindo este processo 10 vezes.
Nos logs, vemos que o Hibernate enviaSchool instruções de inserção em vários lotes de tamanho 1, enquanto esperávamos apenas 2 lotes de tamanho 5. Além disso, as instruções de inserçãoStudent também são enviadas em vários lotes de tamanho 2 em vez de 4 lotes de tamanho 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...
Para agrupar todas as instruções de inserção do mesmo tipo de entidade,we should configure the hibernate.order_inserts property.
Podemos configurar a propriedade Hibernate manualmente usandoEntityManagerFactory:
public Properties hibernateProperties() {
Properties properties = new Properties();
properties.put("hibernate.order_inserts", "true");
// Other properties...
return properties;
}
Se estivermos usando Spring Boot, podemos configurar a propriedade em application.properties:
spring.jpa.properties.hibernate.order_inserts=true
Depois de adicionar esta propriedade, teremos 1 lote para inserções deSchool e 2 lotes para inserções deStudent:
"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. Atualização em lote
Agora, vamos passar para as atualizações em lote. Semelhante às inserções em lote, podemos agrupar várias instruções de atualização e enviá-las ao banco de dados de uma só vez.
Para habilitar isso,we’ll configure hibernate.order_updates and hibernate.jdbc.batch_versioned_data properties.
Se estivermos criando nossoEntityManagerFactory manualmente, podemos definir as propriedades de maneira programática:
public Properties hibernateProperties() {
Properties properties = new Properties();
properties.put("hibernate.order_updates", "true");
properties.put("hibernate.batch_versioned_data", "true");
// Other properties...
return properties;
}
E se estivermos usando Spring Boot, vamos apenas adicioná-los a application.properties:
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.batch_versioned_data=true
Após configurar essas propriedades, o Hibernate deve agrupar instruções de atualização em lotes:
@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());
}
}
Aqui, nós atualizamos as entidades escolares e o Hibernate envia instruções SQL em 2 lotes de tamanho 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. Estratégia de geração de@Id
Quando queremos usar o lote para inserções / atualizações, devemos estar cientes da estratégia de geração de chave primária. If our entities use GenerationType.IDENTITY identifier generator, Hibernate will silently disable batch inserts/updates.
Como as entidades em nossos exemplos usam o gerador de identificadorGenerationType.SEQUENCE, o Hibernate permite operações em lote:
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
8. Sumário
Neste artigo, analisamos inserções e atualizações em lote usando o Hibernate / JPA.
Confira os exemplos de código para este artigoover on Github.