OPTIMIZA TU CAPA DE PERISTENCIA: SACANDO TODO EL PROVECHO A JPA CON HIBERNATE – PARTE 2

En este artículo iré directo a las soluciones, ya que el problema ya lo tenemos claro.

En un mundo ideal, siempre queremos acceder al mismo conjunto de entidades y siempre necesitamos acceder a las mismas entidades hijas para todos los casos de uso, pero en una aplicación real, es muy distinto.

Prosiguiendo con el ejemplo del artículo anterior, en el que somos una comisaría: Es posible que en cierto lugar de la aplicación queramos una lista de personas (para mostrar en una interfaz tan solo los nombres), y en otro lugar de la aplicación necesitemos una lista de personas con sus multas ya cargadas y listas para hacer operaciones con ellas. En el primer caso, nos bastaría con los tipos de fetching por defecto, sin embargo, en el segundo caso, el fetching por defecto nos va a causar el problema N+1.

Un enfoque podría ser indicar el tipo de fetching de multas en Persona a eager, esto nos ahorraría el N+1 para el segundo caso, pero para todos los casos en los que no necesitemos las multas, estaremos cargando información innecesaria que no necesitamos (no es un problema tan grave como en N+1, pero afecta al rendimiento, escalando cuantas más multas tengan las personas accedidas).

Y esto puede ir más lejos. Las multas podrían tener a su vez listas de entidades, y las personas también. Definir una sola estrategia que cubra todos los casos de uso no es posible, por ello, debemos indicar en cada llamada qué entidades y subentidades cargar, y aquí es donde los entity graphs llegan a cubrirnos las espaldas.

Los gloriosos entity graphs

Los entity graphs definen un gráfico de las entidades que se van a cargar en la llamada.

Necesitamos obtener solo la información necesaria, y necesitamos obtenerla toda en un solo bloque

Para usarlos, hay varias maneras: dos de ellas son equivalentes, de modo que de estas solo explicaré la más sencilla de ambas. La más sencilla es especificar el gráfico en el repositorio (@EntityGraph con attributePaths), y la alternativa es usar @NamedEntityGraph para definirlo en la entidad (de una forma bastante poco visual) y vincularlo desde el repositorio, lo cual para mi gusto son pasos extra innecesarios. Sabiendo esto, vamos al grano.

Tan solo define el attribute path

Dado un repositiorio, el de personas en nuestro caso, puede que tengamos las siguiente query:

@EntityGraph(attributePaths = "multas")
List<Persona> findByNombreConMultasCargadas(String nombre);

@EntityGraph(attributePaths = "documentos")
List<Persona> findByNombreConDocumentosCargados(String nombre);

Como puedes observar, la anotación señala una variable, que se cargará de forma eager. Pero la cosa va más allá. Podemos concatenar los hijos de esas variables para que también se carguen:

@EntityGraph(attributePaths = "multas.implicados")
List<Persona> findByNombreConMultasCargadasConLosImplicadosDeLasMultasTambienCargados(String nombre);

No me matéis por el nombre del método, pero creo que así queda patente la potencia de esta funcionalidad.

No es magia: Internamente, esta llamada se traduce en la siguiente query HQL:

SELECT p FROM Persona p

LEFT OUTER JOIN Multa m ON m.persona = p

LEFT OUTER JOIN Implicado ON i.multa = m

Define ramas en el gráfico, pero con cuidado

¿Qué ocurre cuando queremos cargar multas y documentos a la vez? El gráfico correcto tendría dos ramas: Persona -> Multa y Persona -> Documento. Es decir:

@EntityGraph(attributePaths = {"multas", "documentos"})
List findByNombreConMultasYDocumentosCargados(String nombre);
Define todas las ramificaciones y definirás el gráfico

Pero esto puede ser muy complicado, porque es difícil no caer en el problema del producto cartesiano. Y esto es porque al ramificar, deja de ser posible hacer un LEFT OUTER JOIN, obligándonos a hacer dos JOIN FETCH y después agrupar las entidades. Y esto, es muy duro, porque el rendimiento cae exponencialmente. Tan solo piensa en tener 100 multas y 100 documentos, acabarías con un resultado de (100×100) 10000 lineas, que después se agruparían.

Por defecto, si tienes dos List<Entidad> marcadas como eager, Hibernate va a lanzarte una excepción en el arranque: MultipleBagFetchException, pero con los entity graph, esto ocurre en tiempo de ejecución.

Truco con trampa: Si las entidades se guardan en un Set<Entidad> en lugar de una lista, Hibernate permitirá hacer la llamada, pero no debería hacerse, ya que esto no te librará del crear un producto cartesiano.

Entonces, ¿cómo hacer esto de forma correcta? Este problema tiene varias soluciones, dependiendo del tipo de relación que tengan las tablas. En la tercera y última parte de este artículo intentaré explicar de una forma clara cuáles son y cuál es la mejor en cada situación, y adicionalmente comentaré cómo tener dos entity graphs para la misma query.

Espero que te esté resultando útil esta serie; nos vemos pronto, en cuanto la finalice.

Sé el primero en comentar

Deja un comentario

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