Optimización de consultas JPA con cálculos de agregación escalables

Una implementación deficiente puede requerir reescrituras o parches que degraden el rendimiento. Una arquitectura robusta, en cambio, debería permitir agregar nuevos componentes como bloques de construcción, manteniendo la estabilidad general.

Este artículo muestra cómo enriquecer una interfaz de listado listarSolucionesPorPagina con cálculos de agregación de precios, siguiendo un enfoque de "múltiples pasos de consulta" que garantiza escalabilidad y rendimiento.

Versión 1.0: Interfaz optimizada con consulta en múltiples pasos

Antes de la extensión, la interfaz ya estaba optimizada para mostrar información de creadores (SolucionUsuario) e IDs de demandas relacionadas (RelacionSolucionDemanda) en listas paginadas de Solucion.

Se utilizaba un enfoque de tres pasos para evitar problemas N+1:

// SolucionServicio.java (V1.0)
@Transactional(readOnly = true)
public Page<SolucionDTO> listarSolucionesPorPagina(...) {
    // Paso 1: Paginación de entidades principales
    Page<Solucion> paginaEntidades = repositorioSolucion.findAll(spec, pageable);
    List<Solucion> solucionesPagina = paginaEntidades.getContent();
    List<Integer> idsSoluciones = ...;

    // Paso 2: Carga por lotes de SolucionUsuario
    repositorioSolucion.cargarConUsuarios(solucionesPagina);

    // Paso 3: Carga por lotes de IDs de demandas relacionadas
    List<RelacionSolucionDemanda> relaciones = repositorioRelacion.buscarPorIdsSoluciones(idsSoluciones);
    Map<Integer, Set<Integer>> mapaSolucionDemandaIds = ...;

    // Ensamblar DTOs en memoria...
}

Nuevo requisito: Incorporar cálculos de precios

El requisito adicional es mostrar precio por unidad y monto total para cada Solucion, calculados como la suma de (cantidad * precioGrupoCompraApp) de todos sus SolucionItem.

Enfoque incorrecto: Realizar cálculos individuales dentro del bucle de mapeo generaría un nuevo problema N+1.

Versión 2.0: Expansión modular mediante un cuarto paso

La solución preserva la arquitectura existente y añade el cálculo de precios como un componente adicional.

1. Nuevo método de repositorio para cálculo por lotes

// RepositorioSolucion.java
/**
 * Calcula precios por unidad para un lote de IDs de soluciones.
 */
@Query("SELECT s.id, COALESCE(SUM(sp.precioGrupoCompraApp * si.cantidad), 0.0) " +
       "FROM Solucion s " +
       "LEFT JOIN s.items si " +
       "LEFT JOIN si.producto sp " +
       "WHERE s.id IN :idsSoluciones " +
       "GROUP BY s.id")
List<Object[]> calcularPreciosPorLotes(@Param("idsSoluciones") List<Integer> idsSoluciones);

Detalles técnicos:

  • WHERE s.id IN :idsSoluciones: Calcula solo para las soluciones de la página actual
  • GROUP BY s.id: Permite cálculos independientes por solución
  • COALESCE(..., 0.0): Maneja casos sin productos retornando 0

2. Integración en la capa de servicio

// SolucionServicio.java (V2.0)
@Transactional(readOnly = true)
public Page<SolucionDTO> listarSolucionesPorPagina(...) {
    // Pasos 1-3: Inalterados...
    
    // ==================== NUEVO PASO ====================
    // Cálculo por lotes de precios para todas las soluciones
    List<Object[]> resultadosPrecios = repositorioSolucion.calcularPreciosPorLotes(idsSoluciones);
    Map<Integer, BigDecimal> mapaPrecios = resultadosPrecios.stream()
            .collect(Collectors.toMap(
                    fila -> (Integer) fila[0],
                    fila -> (BigDecimal) fila[1]
            ));
    // ====================================================
    
    // Ensamblado extendido de DTOs
    List<SolucionDTO> listaDTOs = solucionesPagina.stream()
            .map(solucion -> {
                SolucionDTO dto = convertirADTO(solucion);
                
                // Lógica extendida
                BigDecimal precioUnitario = mapaPrecios.getOrDefault(
                    solucion.getId(), BigDecimal.ZERO);
                dto.setPrecioUnitario(precioUnitario);
                dto.setMontoTotal(precioUnitario.multiply(
                    new BigDecimal(solucion.getCantidadConjuntos())));
                
                return dto;
            })
            .collect(Collectors.toList());

    return new PageImpl<>(listaDTOs, ...);
}

Resultado: Consultas SQL optimizadas y predecibles

El registro SQL muestra claramente el enfoque de cuatro pasos:

-- 1. Paginación principal
SELECT ... FROM solucion WHERE ... LIMIT ?

-- 2. Carga por lotes de usuarios
SELECT ... FROM solucion ... LEFT JOIN solucion_usuario ... WHERE solucion0_.id IN (...)

-- 3. Carga por lotes de relaciones de demanda
SELECT ... FROM relacion_solucion_demanda ... WHERE relacion.solucion_id IN (...)

-- 4. Cálculo de agregación de precios
SELECT s.id, COALESCE(SUM(...), 0.0) FROM solucion s ... WHERE s.id IN (...) GROUP BY s.id

Características clave:

  • Interacciones controladas: De 3 a 4 consultas fijas, independientes del tamaño de página
  • Responsabilidad única: Cada consulta maneja una tarea específica
  • Rendimiento garantizado: Todas las consultas son por lotes (cláusula IN)

Principios de arquitectura extensible

Esta evolución demuestra la eficacia del enfoque de múltiples pasos:

  1. Escalabliidad: Nuevos requisitos se integran como pasos adicionales sin reestructurar el código existente
  2. Alta cohesión y bajo acoplamiento: Cada paso se enfoca en una tarea específica, comunicándose mediante identificadores simples
  3. Rendimiento sostenible: La adición de pasos mantiene el número de interacciones con la base de datos constante y predecible

Etiquetas: JPA Hibernate rendimiento ConsultasSQL Arquitectura

Publicado el 6-12 19:27