Introducción
En este artículo, aprenderemos a realizar inserciones y actualizaciones por lotes utilizando Hibernate/JPA. Esta técnica es esencial para optimizar la comunicación entre nuestra aplicación y la base de datos, ya que permite enviar un grupo de declaraciones SQL en una sola llamada de red, optimizando así el uso de la red y el consumo de memoria de nuestra aplicación.
1. Overview
En este tutorial, aprenderemos cómo podemos realizar inserciones y actualizaciones por lotes usando Hibernate/JPA. La funcionalidad de procesamiento por lotes es una poderosa herramienta para los desarrolladores de Java que trabajan con bases de datos, pues reduce significativamente el tiempo de ejecución de operaciones masivas y al mismo tiempo mejora el uso de recursos en la memoria.
2. Setup
2.1. Sample Data Model
Empezaremos definiendo el modelo de datos que utilizaremos en los ejemplos. Crearemos una entidad denominada School
:
@Entity
public class School {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
@OneToMany(mappedBy = "school")
private List students;
// Getters y setters...
}
Cada School
tendrá cero o más Student
s:
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String name;
@ManyToOne
private School school;
// Getters y setters...
}
2.2. Tracing SQL Queries
Cuando ejecutamos nuestros ejemplos, necesitamos verificar que las declaraciones de inserción/actualización se envían efectivamente en lotes. Para esto utilizaremos un proxy de DataSource
que trace las declaraciones SQL de 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();
}
// Otros métodos...
}
3. Default Behaviour
Es importante mencionar que Hibernate no habilita el procesamiento por lotes de forma predeterminada. Esto significa que enviará una declaración SQL separada para cada operación de inserción/actualización:
@Transactional
@Test
public void whenNotConfigured_ThenSendsInsertsSeparately() {
for (int i = 0; i < 10; i++) {
School school = createSchool(i);
entityManager.persist(school);
}
entityManager.flush();
}
Al persistir 10 entidades School
, podemos ver en los registros de consulta que Hibernate envía cada declaración de inserción por separado:
"querySize": 1, "batchSize": 0, "query": ["insert into school (name, id) values (?, ?)"],
"params": [["School1","1"]]
Por lo tanto, debemos configurar Hibernate para habilitar el procesamiento por lotes. Esto se logra configurando la propiedad hibernate.jdbc.batch_size
a un número mayor que 0.
Si estamos creando el EntityManager
manualmente, debemos añadir hibernate.jdbc.batch_size
a las propiedades de Hibernate:
public Properties hibernateProperties() {
Properties properties = new Properties();
properties.put("hibernate.jdbc.batch_size", "5");
// Otras propiedades...
return properties;
}
Si estamos utilizando Spring Boot, podemos definirlo como una propiedad de la aplicación:
spring.jpa.properties.hibernate.jdbc.batch_size=5
4. Batch Insert for Single Table
4.1. Batch Insert Without Explicit Flush
Primero, veamos cómo utilizar inserciones por lotes cuando solo tratamos con un tipo de entidad. Utilizaremos el código anterior, pero esta vez habilitaremos el batching:
@Transactional
@Test
public void whenInsertingSingleTypeOfEntity_thenCreatesSingleBatch() {
for (int i = 0; i < 10; i++) {
School school = createSchool(i);
entityManager.persist(school);
}
}
Al persistir 10 entidades School
, en los registros podremos verificar que Hibernate envía las declaraciones de inserción en 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"]]
Un aspecto importante a considerar aquí es el consumo de memoria. Cuando persistimos una entidad, Hibernate la almacena en el contexto de persistencia. Si persistimos 100,000 entidades en una sola transacción, terminaremos teniendo 100,000 instancias de entidad en memoria, lo que podría causar una OutOfMemoryException
.
4.2. Batch Insert With Explicit Flush
Ahora examinamos cómo optimizar el uso de memoria durante las operaciones de batching. El contexto de persistencia almacena entidades recién creadas y modificadas en memoria. Hibernate envía estos cambios a la base de datos cuando se sincroniza la transacción. Sin embargo, llamar a EntityManager.flush()
también activa una sincronización de la transacción.
Para reducir la carga de memoria durante las operaciones por lotes, podemos llamar a EntityManager.flush()
y EntityManager.clear()
en nuestro código cuando se alcance el tamaño del lote:
@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);
}
}
Aquí, estamos vaciando las entidades en el contexto de persistencia, lo que hace que Hibernate envíe consultas a la base de datos. Al limpiar el contexto de persistencia, eliminamos las entidades School
de memoria, manteniendo el comportamiento de batching.
5. Batch Insert for Multiple Tables
Vamos a ver cómo podemos configurar inserciones por lotes cuando se trata de múltiples tipos de entidades en una sola transacción.
Cuando queremos persistir entidades de varios tipos, Hibernate crea un lote diferente para cada tipo de entidad. Esto se debe a que solo puede haber un tipo de entidad en un solo lote. Además, Hibernate crea un nuevo lote cada vez que encuentra un tipo de entidad diferente al del lote actual, incluso si ya hay un lote para ese tipo de entidad:
@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);
}
}
En este código, estamos insertando una School
, asignándole dos Student
s, y repitiendo este proceso 10 veces. En los registros, veremos que Hibernate envía las declaraciones de inserción de School
en varios lotes de tamaño 1, mientras que esperábamos solo 2 lotes de tamaño 5.
Para agrupar todas las declaraciones de inserción del mismo tipo de entidad, debemos configurar la propiedad hibernate.order_inserts
:
public Properties hibernateProperties() {
Properties properties = new Properties();
properties.put("hibernate.order_inserts", "true");
// Otras propiedades...
return properties;
}
Si estamos usando Spring Boot, configuramos la propiedad en application.properties
:
spring.jpa.properties.hibernate.order_inserts=true
6. Batch Update
Ahora pasemos a las actualizaciones por lotes. Al igual que con las inserciones por lotes, podemos agrupar varias declaraciones de actualización y enviarlas a la base de datos de una sola vez. Para habilitar esto, configuraremos las propiedades hibernate.order_updates
y hibernate.batch_versioned_data
.
public Properties hibernateProperties() {
Properties properties = new Properties();
properties.put("hibernate.order_updates", "true");
properties.put("hibernate.batch_versioned_data", "true");
// Otras propiedades...
return properties;
}
Si estamos usando Spring Boot, simplemente añadimos estas líneas a application.properties
:
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.batch_versioned_data=true
Después de configurar estas propiedades, Hibernate debería agrupar las declaraciones de actualización en 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());
}
}
En este caso, hemos actualizado las entidades de las escuelas, y Hibernate envía las sentencias SQL en 2 lotes de tamaño 5.
7. @Id Generation Strategy
Cuando queremos usar batching para inserciones, debemos estar conscientes de la estrategia de generación de la clave primaria. Si nuestras entidades utilizan el generador de identificadores GenerationType.IDENTITY
, Hibernate deshabilitará silenciosamente las inserciones por lotes. Dado que las entidades en nuestros ejemplos utilizan el generador de identificadores GenerationType.SEQUENCE
, Hibernate habilita las operaciones por lotes.
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
8. Summary
En este artículo, hemos explorado las inserciones y actualizaciones por lotes utilizando Hibernate/JPA. Esta capacidad nos permite mejorar el rendimiento de nuestras aplicaciones Java al reducir la cantidad de comunicaciones entre la aplicación y la base de datos, además de optimizar el uso de recursos en memoria. Recuerde siempre configurar adecuadamente Hibernate para aprovechar al máximo su capacidad de procesamiento por lotes y mantener un monitoreo sobre el consumo de memoria en operaciones masivas.
En resumen, seguir estas pautas puede ayudar a los desarrolladores a maximizar la eficiencia y efectividad de las aplicaciones Java que interactúan con bases de datos complejas. ¡Feliz programación!
Para más detalles y ejemplos sobre Hibernate y JPA, consulte este enlace.