Contenido del curso

Spring Data Repositories

Lazy vs Eager en relaciones JPA

Resumen

Cuando trabajas con entidades JPA que tienen relaciones one to one, many to one o one to many, la forma en que recuperas esos datos define el rendimiento de tu aplicación. Aquí aprendes a usar fetch type lazy y eager en JPA con Hibernate para evitar consultas innecesarias y llamados infinitos al serializar entidades en una API REST de Spring Boot.

¿Por qué aparece un llamado infinito al serializar entidades JPA?

Al exponer directamente una entidad como OrderEntity a través de un controlador REST, Jackson intenta convertir el objeto Java a JSON usando los métodos getter. Si OrderEntity tiene una lista de OrderItemEntity y cada OrderItemEntity referencia de vuelta a OrderEntity, se genera un ciclo infinito de serialización.

La solución práctica es anotar la propiedad inversa con @JsonIgnore. En el ejemplo de la pizzería, se aplica sobre el atributo order dentro de OrderItemEntity para cortar el ciclo [3:00].

¿Qué hace @JsonIgnore en una entidad JPA? Le indica a Jackson que ignore esa propiedad al convertir el objeto a JSON, evitando ciclos infinitos cuando hay relaciones bidireccionales.

En un proyecto real, lo recomendable es crear un DTO o clase de dominio que represente solo los datos que quieres exponer, en lugar de serializar las entidades directamente.

¿Cuál es la diferencia entre fetch type lazy y eager?

Aunque uses @JsonIgnore para ocultar una relación del JSON, Hibernate puede seguir consultando esa información en la base de datos. Eso pasa porque la estrategia de recuperación se controla con el atributo fetch de la anotación de relación, no con la serialización.

  • Lazy: la relación no se carga hasta que invocas explícitamente su getter. Si nunca la usas, Hibernate no ejecuta el select asociado.
  • Eager: la relación se carga automáticamente al recuperar la entidad principal, generando la consulta junto con la consulta inicial.

En la clase, el atributo customer de OrderEntity se marca como FetchType.LAZY porque solo se necesita el identificador, mientras que la lista de items se marca como FetchType.EAGER porque siempre se quiere mostrar junto con la orden [6:30].

¿Cuáles son los valores por defecto del fetch type en JPA?

JPA y Hibernate definen estrategias predeterminadas según el tipo de relación. Conocerlas evita sorpresas de rendimiento:

  • @OneToMany y @ManyToMany: por defecto son lazy.
  • @ManyToOne y @OneToOne: por defecto son eager.

Por eso, en el ejemplo, customer se cargaba automáticamente aunque tuviera @JsonIgnore: la relación @ManyToOne es eager por defecto y Hibernate lanzaba cinco consultas extra a la tabla de clientes.

¿Cómo construir el repositorio, servicio y controlador de orders?

La clase arma el flujo completo en Spring Boot para probar el comportamiento. El paso a paso es directo:

  1. Crear la interfaz OrderRepository que extiende ListCrudRepository<OrderEntity, Integer>, con lo cual ya dispones de métodos como findAll sin escribir código adicional.
  2. Crear OrderService anotado con @Service, inyectando el repositorio como final por constructor y exponiendo un método que retorna List<OrderEntity> usando findAll.
  3. Crear OrderController anotado como @RestController, atendiendo en api/orders con un @GetMapping que retorna un ResponseEntity.ok con la lista.

Para que la serialización funcione sin errores, las entidades deben tener las anotaciones de Lombok @Getter, @Setter y @NoArgsConstructor [1:15].

¿Qué hace ListCrudRepository en Spring Data? Es una interfaz que provee operaciones CRUD básicas devolviendo listas, sin que tengas que implementar métodos como findAll, save o deleteById.

¿Cómo verificar que lazy y eager funcionan en consola?

La mejor forma de comprobar el comportamiento es observar las consultas SQL que Hibernate imprime. Después de marcar customer como lazy y items como eager, al llamar GET api/orders desde Postman:

  • Hibernate ejecuta una consulta a pizza_order.
  • Ejecuta seis consultas a order_item, una por cada orden, porque la relación es eager.
  • No ejecuta consultas a la tabla de clientes mientras no se invoque getCustomer().

Si dentro del servicio agregas un forEach que llame order.getCustomer().getName(), Hibernate dispara las consultas a clientes solo en ese momento. Y aquí viene lo interesante: aunque haya seis órdenes, solo se hacen cinco consultas porque uno de los clientes, Drew Watson, aparece en dos órdenes y Hibernate lo mantiene en memoria tras la primera carga [9:45].

¿Cuándo conviene usar lazy en lugar de eager?

La recomendación práctica es usar lazy por defecto en las relaciones que no necesitas siempre, y reservar eager solo para las que acompañan inevitablemente a la entidad principal. Cargar relaciones que no usas equivale a selects gratis contra la base de datos, y eso pesa cuando el tráfico crece.

Usa solo las relaciones que estrictamente necesites en tu modelo, y cuando las declares, anótalas con FetchType.LAZY para mejorar el rendimiento de la aplicación.

¿Has tenido problemas con consultas N+1 al usar eager en tus proyectos? Cuéntame en los comentarios cómo resolviste el balance entre rendimiento y comodidad.