Insercciones y actualizaciones en Batch con Hibernate
En esta nueva entrada sobre insercciones y actualizaciones en Batch con Hibernate vamos a ver como podemos realizar inserrciones y/o actualizaciones en modo batch de queries sql en nuestra base de datos.
Para poder realizar operaciones de tipo batch sobre nuestra base de datos nos vamos a ayudar de las propiedades de configuración que nos da Hibernate.
Por defecto, cuando tenemos colecciones de datos son envíados a la base de datos uno tras otro, es decir, de forma iterativa. Haciendo uso de las configuraciones de Batch de Hibernate vamos a poder enviar en lotes toda esta información ahorrando tiempos y recursos.
Para ver más información de como usar Hibernate con aplicaciones Spring Boot puedes echar un vistazo a este artículo sobre Spring Data.
Configuración de Hibernate para poder realizar operaciones en Batch
Antes de poder realizar procesamiento Batch en Hibernate es necesario realizar su configuración, para ello vamos a utilizar la propiedad hibernate.jdbc.batch_size.
Si estamos trabajando con Spring Boot podremos realizar la configuración a través de nuestro fichero de propiedades de la siguiente manera:
spring.jpa.properties.hibernate.jdbc.batch_size= 5
En la que el número que añadimos a la propiedad indica la cantidad de registros que guardaremos en cada lote.
O directamente a través de las propiedades de Hibernate:
public Properties hibernateConfiguration() { Properties properties = new Properties(); properties.put("hibernate.jdbc.batch_size", "5"); . return properties; }
Además de añadir esta propiedad de configuración indicando el tamaño del lote del batch, es muy importante que nuestra entidad de Base de Datos tenga informado el campo @Id como GenerationType.SEQUENCE.
Insercciones por defecto con Hibernate
El comportamiento por defecto de Hibernate en insercciones es siempre iterativa, es decir, las insercciones en nuestra base de datos serán seguidas.
Vamos a ver el proceso que hace por defecto Hibernate para realizar las insercciones, para ello vamos a comenzar creando nuestras entidades de Base de Datos.
@Table @Entity @Getter @Setterpublic class Brand{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; private String name; @OneToMany(mappedBy = "brand") private Set<Car> cars; }
@Table @Entity @Getter @Setterpublic class Car{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; private String name; private String model; @ManyToOne private Brand brand; }
Para continuar con la verificación del proceso, vamos a ver como Hibernate realiza las insercciones, para ello vamos a crear un bucle para guardar brands a Base de Datos:
@Test @Transactional public void whenInsertACarLoop_thenSaveCars() { for (int i = 0; i < 10; i++) { Car car = createCar(i); entityManager.persist(car); } carRepository.findAll(); }
Si lanzamos esta ejecución haciendo uso de Spring necesitamos saber si las insercciones que se están realizando son o no de procesos Batch de Hibernate. Para ello vamos a hacer uso de ProxyDataSourceBuilder, para poder capturar esos logs:
private static class HibernateLoggingInterceptor implements MethodInterceptor { private final DataSource dataSource; public ProxyDataSourceInterceptor(final DataSource dataSource) { this.dataSource = ProxyDataSourceBuilder .create(dataSource) .name("Batch process") .asJson() .countQuery() .logQueryToSysOut().build(); } }
Una vez tenemos el logging configurado para poder trazar logs, lanzamos las queries y vemos que se han realizado insercciones en Base de Datos sin Batch:
{"name":"Batch process", "connection":3, "time":2, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into car_batch (brand_id, color, created_date, description, model, modified_date, id) values (?, ?, ?, ?, ?, ?, ?)"], "params":[["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 1","0","1"]]} {"name":"Batch process", "connection":3, "time":2, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into car_batch (brand_id, color, created_date, description, model, modified_date, id) values (?, ?, ?, ?, ?, ?, ?)"], "params":[["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 2","0","2"]]} {"name":"Batch process", "connection":3, "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into car_batch (brand_id, color, created_date, description, model, modified_date, id) values (?, ?, ?, ?, ?, ?, ?)"], "params":[["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 3","0","3"]]} {"name":"Batch process", "connection":3, "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into car_batch (brand_id, color, created_date, description, model, modified_date, id) values (?, ?, ?, ?, ?, ?, ?)"], "params":[["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 4","0","4"]]} {"name":"Batch process", "connection":3, "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into car_batch (brand_id, color, created_date, description, model, modified_date, id) values (?, ?, ?, ?, ?, ?, ?)"], "params":[["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 5","0","5"]]} {"name":"Batch process", "connection":3, "time":2, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into car_batch (brand_id, color, created_date, description, model, modified_date, id) values (?, ?, ?, ?, ?, ?, ?)"], "params":[["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 6","0","6"]]} {"name":"Batch process", "connection":3, "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into car_batch (brand_id, color, created_date, description, model, modified_date, id) values (?, ?, ?, ?, ?, ?, ?)"], "params":[["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 7","0","7"]]} {"name":"Batch process", "connection":3, "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into car_batch (brand_id, color, created_date, description, model, modified_date, id) values (?, ?, ?, ?, ?, ?, ?)"], "params":[["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 8","0","8"]]} {"name":"Batch process", "connection":3, "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into car_batch (brand_id, color, created_date, description, model, modified_date, id) values (?, ?, ?, ?, ?, ?, ?)"], "params":[["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 9","0","9"]]} {"name":"Batch process", "connection":3, "time":1, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["insert into car_batch (brand_id, color, created_date, description, model, modified_date, id) values (?, ?, ?, ?, ?, ?, ?)"], "params":[["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 10","0","10"]]}
Vemos como las insercciones se realizan de manera secuencial, y además vemos en el log como batch es igual a false y el número de batchs a insertar es 0. A continuación vamos a activar nuestra propiedad de batch de Hibernate y vemos como se realizan las insercciones.
Insercciones en Batch con Hibernate
Si partimos del mismo ejemplo de antes en el que con nuestro bucle guardamos 5 registros, el logging será el siguiente:
{"name":"Batch process", "connection":3, "time":20, "success":true, "type":"Prepared", "batch":true, "querySize":1, "batchSize":10, "query":["insert into car (brand_id, color, created_date, description, model, modified_date, id) values (?, ?, ?, ?, ?, ?, ?)"], "params":[["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 1","0","1"],["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 2","0","2"],["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 3","0","3"],["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 4","0","4"],["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 5","0","5"],["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 6","0","6"],["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 7","0","7"],["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 8","0","8"],["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 9","0","9"],["NULL(BIGINT)","NULL(VARCHAR)","0","NULL(VARCHAR)","Mustang 10","0","10"]]}
Por lo general cuando guardamos un nuevo registro en Base de Datos hacemos uso de save o persist. De forma que cuando se termina la transacción todo los registros se guardan en Base de Datos. Esto provoca que mientras estás dentro de una transacción esos registros se guardan en memoria dentro del contexto de persistencia y puede llegar a provocar una excepción de memoria.
Una de las medidas que podemos aplicar para apaliar posibles problemas de memoria cuando guardamos en base de datos haciendo uso de Hibernate es utilizar flush.
Actualizaciones en Batch con Hibernate
Para poder realizar actualizaciones en Batch con Hibernate es necesario activar dos propiedades de Hibernate, hibernate.order_updates y hibernate.batch_versioned_data con valor true.
Y si hacemos uso de Spring Boot las podemos activar mediante nuestro application.yml:
spring.jpa.properties.hibernate.order_updates=true spring.jpa.properties.hibernate.batch_versioned_data=true
Una vez hemos activado estas dos propiedades podremos realizar actualizaciones contra nuestra base de datos haciendo uso del modo Batch de Hibernate.
Conclusión
Insercciones y actualizaciones en Batch con Hibernate nos va ayudar en nuestras aplicaciones a optimizar el rendimiento y el uso de recursos. Los tiempos de guardado son muy importantes por lo que si podemos conseguir guardar en modo batch y además evitar hacer uso de la memoria guardando con flush podremos realizar una mayor optimización de nuestros recursos.
Si quieres ver un ejemplo funcionando que hace uso de Batch en Hibernate puedes verlo en nuestro github aquí. En la parte de test en el package Batch puedes ver el ejemplo funcionado.
Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com o también nos puedes contactar por nuestras redes sociales Facebook o twitter y te ayudaremos encantados!