Contenido del curso

Spring Data Repositories

Projections con JPA para queries de varias tablas

Resumen

Cuando construyes una API con Spring Data JPA, llega el momento en que un findAll o un findById se quedan cortos. Necesitas datos cruzados de varias tablas, en un formato propio, y sin cargar entidades enteras. Ahí entran las projections basadas en interfaces, una forma elegante de devolver DTOs personalizados a partir de queries nativos en Spring Boot.

Esta guía te muestra cómo armar una proyección, escribir el SQL con joins y GROUP_CONCAT, y exponerla como endpoint REST usando un proyecto de pizzería como ejemplo.

¿Qué es una projection en Spring Data JPA y cuándo usarla?

Una projection es una interfaz que define solo los atributos que quieres recuperar en una consulta personalizada. Spring Data JPA se encarga de mapear el resultado del query a esa interfaz, sin que tú escribas la clase de implementación.

¿Qué es una projection? Es una interfaz con métodos getter que representa el resultado a la medida de un query. Spring Data JPA la implementa automáticamente al ejecutar la consulta.

La usas cuando un resultado mezcla columnas de varias tablas y no quieres traer entidades completas. En el ejemplo de la pizzería, el resumen de una orden combina datos de pizza_order, customer, order_item y pizza: identificador, fecha, total, nombre del cliente y los nombres de las pizzas concatenados.

¿Cómo crear una interface projection paso a paso?

Dentro de la capa de persistencia conviene crear un paquete dedicado, por ejemplo projection, donde vivan todos los DTOs de queries personalizados. Así separas claramente las entidades reales de los resultados a la medida.

La interfaz OrderSummary expone los atributos como getters:

  • Integer getIdOrder() para el identificador de la orden.
  • String getCustomerName() para el nombre del cliente.
  • LocalDate getOrderDate() para la fecha de la orden.
  • Double getOrderTotal() para el valor total.
  • String getPizzaNames() para los nombres de pizzas concatenados.

No tienes que respetar los nombres de columnas de la base de datos. Lo único innegociable es que el alias del SELECT coincida con el nombre del getter (sin el prefijo get y con la primera letra en minúscula).

¿Cómo escribir el query nativo con @Query y joins?

El SQL que alimenta la proyección hace tres joins: une pizza_order con customer por el id del cliente, con order_item por el id de la orden, y con pizza por el id de la pizza. El filtro es un WHERE sobre el id de la orden recibido como parámetro.

Para concatenar los nombres de las pizzas en una sola fila se usa la función GROUP_CONCAT de MySQL, que une varios registros separados por coma. Por eso aparece un GROUP BY con el id de la orden, el nombre del cliente, la fecha y el total.

sql SELECT po.id_order AS idOrder, c.name AS customerName, po.date AS orderDate, po.total AS orderTotal, GROUP_CONCAT(p.name SEPARATOR ', ') AS pizzaNames FROM pizza_order po INNER JOIN customer c ON po.id_customer = c.id_customer INNER JOIN order_item oi ON po.id_order = oi.id_order INNER JOIN pizza p ON oi.id_pizza = p.id_pizza WHERE po.id_order = :orderId GROUP BY po.id_order, c.name, po.date, po.total

En el repositorio, la firma queda corta y declarativa:

java @Query(value = "SELECT po.id_order AS idOrder, ... WHERE po.id_order = :orderId GROUP BY ...", nativeQuery = true) OrderSummary findSummary(@Param("orderId") int orderId);

La anotación @Query con nativeQuery = true le dice a Spring que ejecute SQL puro, y @Param enlaza el parámetro del método con el placeholder :orderId.

¿Por qué cuidar los espacios y los alias en un nativeQuery?

Cuando partes un SQL largo en varias líneas dentro de un String de Java, cada línea se concatena. Si olvidas un espacio al final, palabras como pizzaNames y FROM quedan pegadas y el query falla. IntelliJ ayuda pintando las palabras reservadas en naranja solo cuando la sintaxis es válida.

Otro detalle clásico al copiar y pegar es arrastrar un tabulador delante de un alias y terminar con tpo.idOrder cuando tu alias real es po. El error sale como unknown column y se resuelve borrando ese carácter invisible.

¿Cómo exponer la projection en service y controller?

El flujo se mantiene limpio: el repositorio devuelve la proyección, el servicio la propaga y el controlador la entrega como JSON.

En el servicio, un método getSummary(int orderId) simplemente llama a orderRepository.findSummary(orderId). En el controlador, un endpoint GET mapeado a /summary/{id} recibe el identificador, invoca al servicio y retorna un OrderSummary.

¿Cómo paso un parámetro a un @Query nativo? Declaras el parámetro en el método del repositorio con @Param("orderId") y lo referencias en el SQL como :orderId. El nombre debe coincidir exactamente.

Al probar en Postman con GET /summary/2, la respuesta trae el id 2, el cliente Johanna Rains, la fecha, el total y los nombres de las pizzas separados por coma. Todo en una sola consulta a base de datos.

¿Qué ventajas tienen las projections frente a entidades completas?

Delegar el trabajo a la base de datos cambia el rendimiento de tu aplicación. En lugar de cargar la entidad PizzaOrder con sus relaciones a Customer, OrderItem y Pizza, y luego procesar todo en Java, recibes exactamente los campos que vas a serializar.

  • Flexibilidad: defines DTOs a la medida sin clases adicionales.
  • Legibilidad: la interfaz documenta qué devuelve cada query.
  • Rendimiento: menos columnas viajan, menos objetos se instancian, menos joins fantasma genera Hibernate.

¿Has tenido que armar resúmenes que cruzan tres o cuatro tablas en tus proyectos? Cuéntame en los comentarios cómo lo resolviste antes de conocer las projections.