Este artículo puede incluir enlaces de afiliado. Si compras a través de ellos, me llevo una pequeña comisión sin coste extra para ti. Más información.
Hay configuraciones que llevan años funcionando en tus proyectos y nunca te paras a mirarlas. Están ahí, en verde, haciendo su trabajo silencioso. Hasta que un día abres una traza de logs y ves cuarenta queries donde esperabas una. Eso me pasó con Open Session in View, un interruptor que Spring Boot deja encendido por defecto y que yo llevaba media carrera profesional sin cuestionar.
No es un bug. No es un fallo de nadie. Es una decisión de diseño que resulta cómoda al principio y que, con el tiempo, esconde debajo de la alfombra un problema serio de rendimiento. Voy a contar cómo funciona, qué hace por debajo, y por qué tardé tanto en darme cuenta de que mis consultas se estaban ejecutando en el sitio más raro posible: la capa de presentación.
Qué es Open Session in View y por qué está encendido sin que lo pidieras
Cuando arrancas una aplicación Spring Boot con JPA e Hibernate, hay un componente llamado OpenEntityManagerInViewInterceptor que se registra solo. Su trabajo es mantener abierta la sesión de Hibernate durante todo el ciclo de vida de la petición HTTP.
Traducido: la conexión con la base de datos, o mejor dicho el EntityManager, sigue viva desde que entra la request hasta que se termina de renderizar la respuesta. No se cierra al salir de la capa de servicio, que es donde uno intuitivamente pensaría que acaba el trabajo con datos.
Spring lo activa por defecto. Ni siquiera te avisa demasiado. Si miras el log de arranque en algunas versiones verás un mensaje tipo «spring.jpa.open-in-view is enabled by default», pero seamos honestos: nadie lee ese log línea a línea el día que monta el proyecto.
La consecuencia práctica es que puedes acceder a relaciones LAZY de tus entidades desde el controlador, o incluso desde la plantilla, y funciona. No salta la temida LazyInitializationException. Y ahí empieza el problema, porque lo que funciona sin quejarse tiende a no revisarse nunca.
El lazy loading, la comodidad engañosa y las queries que no ves
Para entender por qué esto duele hay que recordar cómo funciona el lazy loading. Cuando marcas una relación como FetchType.LAZY, Hibernate no la carga hasta que alguien la toca. En su lugar deja un proxy.
Mientras la sesión esté abierta, ese proxy puede resolverse. Va a la base de datos, lanza la query, trae los datos. Perfecto en teoría. El problema es cuándo y cuántas veces se resuelve ese proxy.
Imagina una entidad Pedido con una lista LAZY de Linea. Cargas cien pedidos en el servicio. Devuelves la lista al controlador. Y luego, al serializar el JSON o al pintar la vista, tocas pedido.getLineas() para cada pedido.
Cien queries adicionales. Una por pedido. El famoso problema N+1, disfrazado de código limpio. Y lo peor es que con Open Session in View activado no te enteras, porque la sesión sigue abierta y todo se resuelve sin lanzar excepciones.
@Entity
public class Pedido {
@Id
private Long id;
// Relación LAZY: no se carga hasta que alguien la toca
@OneToMany(mappedBy = "pedido", fetch = FetchType.LAZY)
private List<Linea> lineas;
}
// En el servicio devolvemos los pedidos sin cargar las líneas
@Transactional(readOnly = true)
public List<Pedido> buscarTodos() {
return pedidoRepository.findAll(); // 1 query
}
// Y en la vista/serialización tocamos lineas de cada pedido...
// -> N queries extra, una por pedido, fuera de la transacción
Ese código de arriba parece inocente. Y lo es, hasta que multiplicas. Con Open Session in View apagado, ese getLineas() en la capa de presentación reventaría con una LazyInitializationException y te obligaría a resolver la carga donde toca: en el servicio, dentro de la transacción.
Con él encendido, no revienta. Se ejecuta. Silenciosamente. En la capa equivocada, y con la conexión de base de datos retenida mucho más tiempo del necesario.

Cómo me di cuenta (más tarde de lo que me gustaría admitir)
La historia real es poco épica. Estaba mirando un endpoint que iba lento en producción, uno de esos que devuelve un listado con detallitos anidados. Nada crítico, pero el tiempo de respuesta bailaba entre 200 milisegundos y casi dos segundos según el volumen.
Activé el log de SQL de Hibernate poniendo spring.jpa.show-sql=true y logging.level.org.hibernate.SQL=DEBUG. Y ahí estaba. Una cascada de select que no había escrito nadie, o mejor dicho, que había escrito yo sin saberlo al tocar getters en el mapeo de la respuesta.
Lo que más me chocó no fue el N+1. Ese lo conocía de sobra. Lo que me descolocó fue dónde se estaban lanzando esas queries. No en el servicio. No en el repositorio. Se disparaban durante la serialización, cuando la transacción de negocio ya había terminado hacía rato.
Encontré, buscando, un artículo de dev.to titulado «Open Session in View Is Convenient — Until It Hides Your Lazy Loading Problem» que describía casi calcado lo que me estaba pasando. Da gusto ver que no eres el único que ha tropezado con la misma piedra cómoda.
Por qué es una comodidad peligrosa y no solo una molestia
Podría parecer un problema menor de rendimiento. Unas queries de más, qué más da. Pero hay tres consecuencias que no me gustan nada de mantener este comportamiento por defecto.
La primera es la retención de conexiones. La conexión de base de datos se devuelve al pool cuando termina la petición, no cuando termina la lógica de negocio. Si tu renderizado es lento, o tu vista tiene mucho que serializar, estás ocupando una conexión más tiempo del que deberías.
En un pool con veinte conexiones y tráfico alto, eso se traduce en peticiones que esperan a que se libere una conexión. Un cuello de botella difícil de diagnosticar porque el síntoma aparece lejos de la causa.
La segunda es que difumina la frontera entre capas. Tu capa de presentación acaba tocando la base de datos sin que sea evidente. La separación de responsabilidades queda como un adorno bonito en el diagrama de arquitectura, pero no en el comportamiento real.
Y la tercera, la que más me molesta, es que oculta errores de diseño en desarrollo. Escribes código que carga mal los datos, lo pruebas, funciona, y nunca ves el aviso que te diría que lo estás haciendo regular. El problema aparece meses después, en producción, con datos de verdad.
«Los programas deben escribirse para que los lean las personas, y solo de forma incidental para que los ejecuten las máquinas.»
— Harold Abelson
Esta frase de Abelson viene muy al caso. Open Session in View hace que el código se ejecute bien pero se lea mal, porque esconde dónde ocurren de verdad las cosas. Un getter que parece inocente en la vista está tocando la base de datos, y eso no lo cuenta el código a quien lo lee. La comodidad se paga en claridad.
Qué hacer con esto: apagarlo, medir y cargar bien
Lo primero que hice fue lo más agresivo posible: apagarlo en un entorno de desarrollo y ver qué se rompía.
// En application.properties
// spring.jpa.open-in-view=false
// Al desactivarlo, esto peta con LazyInitializationException
// si intentas tocar lineas fuera de la transacción:
List<Pedido> pedidos = pedidoService.buscarTodos();
pedidos.get(0).getLineas().size(); // boom
// La solución correcta: cargar lo que necesitas en el servicio,
// dentro de la transacción, con un fetch join explícito.
@Query("select p from Pedido p left join fetch p.lineas")
List<Pedido> findAllConLineas();
Poner spring.jpa.open-in-view=false hace que las cosas se rompan pronto y donde tienen que romperse. Sí, aparecen las LazyInitializationException. Sí, es incómodo. Pero cada excepción es un cartel señalando exactamente dónde estabas cargando datos de forma perezosa en el sitio equivocado.
A partir de ahí, la solución no es marcar todo como EAGER, que es el error contrario y trae sus propios demonios. La solución es cargar de forma explícita lo que cada caso de uso necesita, dentro de la transacción, con fetch join, con @EntityGraph, o con proyecciones DTO.
Foto: turned-on gray laptop computer por Safar Safarov en Unsplash.
¿Quieres más?
Recibe los nuevos artículos sobre gatos, escritura y código. Sin frecuencia fija, sin paja de IA, sin spam.
Suscribirme