Errores más comunes con Hibernate


Hibernate es un ORM que nos va ayudar mucho en la implementación y modelado de nuestra base de datos en nuestras aplicaciones, pero mal usado nos puede dar muchos dolores de cabez. En esta entrada vamos a ver los errores más comunes con Hibernate, desde problemas de rendimiento, queries excesivas o maneras de mejorar o ahorrar líneas de código.

Usar Fetching Eager

El uso de FetchType definida como Eager suele ser de los errores más comunes con Hibernate. La definición de Eager impactará de manera crucial en el rendimiento de nuestras aplicaciones, repercutiendo en el proceso y funcionamiento de toda nuestra aplicación.

Cuando definimos una relación @OneToMany, @ManyToOne, @ManyToMany o @OneToOne, podemos definir el atributo FetchType como Eager, lo que significa que Hibernate realizará una carga completa al cargar una entidad.

En los casos en los que tenemos muchos hijos o listas asociadas y a su vez más hijos se realizará una carga de todo, lo que tendrá una join

@Entity
public class User{
 
    @ManyToMany(mappedBy="users", fetch=FetchType.EAGER)
    private Set<Account> accounts= new HashSet<Account>();
     
    ...
     
}

Por ejemplo, en el caso anterior cada vez que recuperemos un usuario recuperaremos todas las cuentas asociadas, quizás lo necesitamos así, y por eso tenemos EAGER, pero si Account tiene más listas y tenemos puesto EAGER podrá traer toda una BBDD para ese usuario. Y en estos casos se penalizará el rendimiento.

Para evitar los problemas con EAGER, lo mejor es hacer uso de FetchType.LAZY. El cual va a retrasar la inicialización de la relación hasta que se solicite, esto evitará joins o queries inncesarias.

Por defecto JPA define LAZY para las relaciones, así que, mejor no tocarla e intentar trabajar con LAZY.

Cambiar el EAGER por defecto de las relaciones ManyToOne y OneToOne

Por defecto en las relaciones la parte que lleva el objeto de la clase a la que hace referencia se carga como EAGER. Se encuentra así por defecto porque por lo general no repercute mucho en el rendimiento ya que suele traerse un registro únicamente, pero el problema aparece cuando traemos muchas entidades y a su vez un objeto, en estos casos sí se puede ver afectado el rendimiento, por lo que es mucho mejor cambiarlo a LAZY.

@Entity
public class Account{
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "fk_user")
    private User user;
     
    ...
     
}

Hacer uso de LAZY sin controlar las respuestas

Algo muy común que vemos cuando activamos las trazas para mostrar las queries en nuestros logs, es que se hacen más peticiones de las que nosotros hemos configurado (n +1 queries), por qué?. Esto es debido a que Hibernate se encuentra realizando una carga «perezosa» de tus colecciones, pero cuando haces un get sobre una relación, en ese momento se hace una query. Este es otro de los errores más comunes con Hibernate que podemos evitar haciendo un análisis de las llamadas que se hacen a nuestras colecciones después de recuperarlas.

Por ejemplo, esta error es muy común cuando usamos mapStruct en nuestra aplicación. Queremos convertir nuestro objecto entidad a Dominio y no hemos ignorado la colección, por lo que mapStruct nos hará un get de nuestra colección haciendo tantas queries como objetos tengamos en nuestra lista.

Por lo que cuando hagamos uso de Fetchtype.LAZY debemos controlar mucho nuestras queries así como nuestros mapeos para evitar que se produzcan queries de más.

@Entity
public class User{
 
    @ManyToMany(mappedBy="users", fetch=FetchType.LAZY)
    private Set<Account> accounts= new HashSet<Account>();
     
    ...
     
}

Si tenemos el código anterior y una clase de mapstruct en la que convertimos User a UserDTO como la siguiente:

@Mapper(componentModel = "spring")
public interface UserMapper{
    UserDto toDto(User user);
}

Estaríamos generando una query extra por cada elemento de la lista de Account.

Hacer uso de flush() al salvar una entidad

Cuando hacemos uso de flush() guardamos la entidad en el momento de crear la sentencia, a veces esto puede ser necesario si necesitamos seguir trabajando con la entidad de alguna manera o una persistencia inmediata. Pero cuando se hace un flush() después de la creación o actualización de una entidad estas obligando a Hibernate a un dirty check, es decir, hacer gestión para mantener la trazabilidad de todas las entidades afectadas. Este proceso repercute en el rendimiento.

Hibernate gestiona todas las insercciones, actualizaciones y borrados en una pila para poder gestionarlas al finalizar la transacción de modo que ahorras tiempo y las gestiona intentando limitar el impacto y el número de operaciones.

Obviamente esto no significa que no debamos hacer uso de flush(), sino que si hay muchas operaciones sí debemos evitarlo delegando a Hibernate esta reponsabilidad.

Eliminar, actualizar o insertar elemento a elemento

Otro de los errores que debemos evitar al hacer uso de Hibernate en nuestras aplicaciones es realizar la insercción, o eliminación o actualización de una lista elemento por elemento.

Tiene sentido que operaciones contra base de datos de una en una sea más costoso que hacerlo a la vez no?, para ello podemos hacer uso de queries nativas o de la funcionalidad batch de Hibernate, la cual nos permite realizar insercciones masivas limitando el número de queries.

Hacer uso de proyecciones en Hibernate

Cuando intentamos optimizar nuestra aplicación es muy importante analizar si en las queries de consulta estamos trayendo más información de la que necesitamos. Obviamente cuanta más información más tiempo durará la petición a Base de Datos.

Para estos casos en los que no se necesita traer toda la información es el perfecto caso para hacer uso de proyecciones.

Por ejemplo:

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<String> query = builder.createQuery(String.class);
Root<User> user= query.from(User.class);
query.select(user.get("name"));
List<String> resultList = entityManager.createQuery(query).getResultList();

Hacer uso de FindAll() sin límite de tamaño

En muchas ocasiones nos dejamos llevar por hacer uso de findAll() o realizar queries sin filtrar el tamaño. Por ejemplo cuando queremos mostrar un listado, esto, impacta negativamente en el rendimiento ya que podemos estrar trayendo demasiados registros.

Para estos casos habría que evitar hacer uso de JPQL, ya que no nos va a permitir realizar paginación, pero haciendo uso de Queries o con funciones de Hibernate vamos a poder pasarle como parámetro un objeto para paginación.

Por ejemplo podemos crear un objeto Pageable con Spring Data de la siguiente manera:

    Pageable pageable = PageRequest.of(page != null ? page : 0, size != null ? size : 50,
        Sort.by(orders));

Uso de Hibernate para todo

Hibernate y JPA son unas herramientas imprescindibles dentro de nuestros desarrollos cuando se requiere conexión a Base de Datos, pero es necesario hacer uso de ellos siempre?.

Lo más importante es conocer muy bien su uso y cuándo hacer uso de ellos para no impactar en el rendimiento de nuestra aplicación. No siempre es necesario hacer uso de Hibernate podemos crear queries nativas de manera que generemos resultados más concretos con lo que podamos mejorar el rendimiento.

Conclusión

En esta entrada sobre errores más comunes con Hibernate hemos visto típicos problemas y errores que cometemos cuando desarrollamos nuestras aplicaciones con Hibernate y JPA. Un mal uso de Hibernate y JPA nos puede llevar a tener cuellos de botella en nuestra aplicación en la capa de acceso a Datos y afectar muy negativamente el resultado final de nuestra aplicación.

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!


Deja una respuesta

Tu dirección de correo electrónico no será publicada.