La lección anterior cerró el módulo 3 con una constatación incómoda: la API de CicloUrbana está completa, documentada y publicable, pero cuando el proceso se reinicia todo desaparece. Las cuatro estaciones de Ribalta vuelven a nacer del CargadorEstacionesDemo, las bicicletas dadas de alta se esfuman y los alquileres del día se pierden. Todo vive en el ConcurrentHashMap de EstacionRepositorioEnMemoria, que existe mientras existe la JVM. Este módulo sustituye esa provisionalidad por persistencia real sobre una base de datos relacional, y lo hace sin tocar ni un controlador, gracias a que desde 02-01 escondimos el almacenamiento tras la interfaz EstacionRepositorio.
Antes de escribir una sola anotación conviene entender el terreno. Esta lección es conceptual y responde a las preguntas que casi nadie se hace antes de empezar y todo el mundo se hace cuando algo falla: por qué existe un ORM, qué significa exactamente cada una de las cuatro siglas que se mezclan a diario —JDBC, JPA, Hibernate, Spring Data JPA—, qué es el contexto de persistencia y por qué una entidad puede estar «gestionada» o «separada», y qué hace Spring Boot por su cuenta en cuanto detecta el starter. Sin esta base, JPA parece magia; con ella, es una máquina previsible.
Contenido
- El desfase objeto-relacional
- Qué es un ORM y qué problema resuelve realmente
- Quién es quién: JDBC, JPA, Hibernate y Spring Data JPA
- Qué aporta
spring-boot-starter-data-jpa EntityManager, unidad de persistencia y contexto de persistencia- Los cuatro estados de una entidad
- Qué genera Spring Boot al detectar el starter
- Alternativas a JPA en el ecosistema Spring
- El modelo de datos objetivo de CicloUrbana
- Errores Comunes y Consejos
- Ejercicios
- El desfase objeto-relacional
En Java, Estacion es un objeto: tiene identidad propia (dos objetos distintos aunque sean iguales campo a campo), referencias directas a otros objetos, herencia, y vive en un grafo que el recolector de basura gestiona. En una base de datos relacional, una estación es una fila en una tabla: tiene identidad por clave primaria, se relaciona con otras filas mediante claves ajenas, no hereda de nada y solo existe dentro de un conjunto de tuplas.
Los dos modelos son buenos, pero no encajan. A esa colección de desencuentros se le llama desfase objeto-relacional (object-relational impedance mismatch), y tiene cinco caras concretas:
| Desencuentro | En Java | En SQL | Consecuencia práctica |
|---|---|---|---|
| Granularidad | Estacion contiene un objeto Ubicacion con latitud y longitud |
La tabla estaciones tiene dos columnas sueltas |
Hay que aplanar o expandir objetos |
| Identidad | == (referencia) y equals() (valor) son cosas distintas |
Solo existe la clave primaria | Dos objetos pueden representar la misma fila |
| Herencia | Incidencia → IncidenciaBateria, IncidenciaVandalismo |
No hay herencia entre tablas | Hay que elegir una estrategia de mapeo |
| Asociación | Referencia dirigida: Bicicleta.estacion |
Clave ajena simétrica, navegable en ambos sentidos | La bidireccionalidad hay que construirla a mano |
| Navegación | bici.getEstacion().getNombre(), salto a salto |
JOIN, todo de una vez |
Navegar objeto a objeto genera muchas consultas |
Un ejemplo de CicloUrbana lo hace tangible. Para obtener el nombre de la estación de una bicicleta, en Java lo natural es:
En SQL lo natural es lo contrario: pedirlo todo de golpe.
SELECT b.matricula, e.nombre
FROM bicicletas b
JOIN estaciones e ON e.id = b.estacion_id
WHERE b.id = 42;La primera forma, ejecutada dentro de un bucle sobre cien bicicletas, produce cien consultas. Ese es el germen del problema N+1 que veremos en 04-04. No es un fallo de JPA: es el desfase asomando.
- Qué es un ORM y qué problema resuelve realmente
Un ORM (Object-Relational Mapping) es una capa que traduce automáticamente entre objetos y filas. Escribes estacionRepositorio.guardar(estacion) y alguien genera el INSERT; lees estacion.getBicicletas() y alguien genera el SELECT.
Para valorar lo que aporta, mira lo que costaría hacerlo a mano con JDBC puro. Este sería el guardar de una estación sin ORM:
public Estacion guardar(Estacion estacion) {
String sql = """
INSERT INTO estaciones (nombre, direccion, capacidad, latitud, longitud)
VALUES (?, ?, ?, ?, ?)
""";
try (Connection conexion = dataSource.getConnection();
PreparedStatement sentencia =
conexion.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
sentencia.setString(1, estacion.getNombre());
sentencia.setString(2, estacion.getDireccion());
sentencia.setInt(3, estacion.getCapacidad());
sentencia.setDouble(4, estacion.getLatitud());
sentencia.setDouble(5, estacion.getLongitud());
sentencia.executeUpdate();
try (ResultSet claves = sentencia.getGeneratedKeys()) {
if (claves.next()) {
estacion.setId(claves.getLong(1));
}
}
return estacion;
} catch (SQLException e) {
throw new AccesoDatosException("No se pudo guardar la estación", e);
}
}Veinte líneas para una tabla de cinco columnas. Multiplícalo por buscarPorId, buscarTodas, actualizar, eliminar, y luego por las seis tablas del modelo. Y todavía falta el ResultSet → objeto, el manejo de nulos, y que cada ALTER TABLE obliga a revisar todas esas posiciones de parámetro numeradas a mano.
Lo que un ORM aporta, ordenado por importancia real:
- Elimina el código repetitivo de mapeo. El 80 % de ese fragmento desaparece.
- Gestiona la identidad y el estado. Sabe qué objetos ha cargado y cuáles han cambiado, y genera el
UPDATEsolo de lo modificado. - Traduce SQL específico de cada motor. El mismo código funciona sobre H2 en desarrollo y PostgreSQL en producción.
- Ofrece un lenguaje de consulta orientado a objetos (JPQL) que se valida contra el modelo, no contra cadenas.
- Automatiza la carga de asociaciones y ofrece control sobre cuándo cargarlas.
Y lo que un ORM no aporta, porque conviene decirlo desde el principio:
- No te exime de conocer SQL. Cuando algo va lento, el diagnóstico se hace leyendo el SQL generado.
- No convierte un modelo relacional malo en uno bueno.
- No es gratis: añade una capa con reglas propias que hay que entender (justo lo que hace este módulo).
- Quién es quién: JDBC, JPA, Hibernate y Spring Data JPA
Estas cuatro piezas se nombran a diario como si fueran intercambiables y no lo son. Cada una vive en un nivel de abstracción distinto y depende de la anterior.
| Pieza | Qué es | Quién la mantiene | Tipo de artefacto | Ejemplo de uso |
|---|---|---|---|---|
| JDBC | API estándar de Java para hablar con una base de datos vía SQL | Especificación de Java SE | API + driver por motor | connection.prepareStatement(sql) |
| JPA (Jakarta Persistence) | Especificación: define anotaciones, EntityManager, JPQL y el ciclo de vida. No ejecuta nada |
Jakarta EE (antes JSR-338) | Interfaces y anotaciones | @Entity, EntityManager.persist() |
| Hibernate | Implementación de JPA: el motor que genera y ejecuta el SQL de verdad | Red Hat | Librería (hibernate-core) |
Es quien escribe el INSERT |
| Spring Data JPA | Abstracción sobre JPA: genera implementaciones de repositorios a partir de interfaces | Spring (VMware/Broadcom) | Librería (spring-data-jpa) |
interface EstacionRepositorio extends JpaRepository<...> |
La distinción crítica es JPA es una especificación, Hibernate es una implementación. JPA define que existe @Entity y qué debe significar; Hibernate escribe el código que hace que signifique eso. Otras implementaciones son EclipseLink y OpenJPA, pero Spring Boot trae Hibernate por defecto y es la elección estándar de facto.
graph TD
A["EstacionService (tu código de negocio)"] --> B["EstacionRepositorio<br/>interfaz Spring Data JPA"]
B --> C["SimpleJpaRepository<br/>implementación generada"]
C --> D["EntityManager<br/>API de JPA (especificación)"]
D --> E["Hibernate<br/>implementación de JPA"]
E --> F["JDBC + driver PostgreSQL"]
F --> G[("PostgreSQL 16")]
Cada flecha hacia abajo es una traducción. estacionRepositorio.findById(1L) se convierte en una llamada a EntityManager.find(Estacion.class, 1L), que Hibernate traduce a un SELECT ... WHERE id = ?, que el driver JDBC envía por el socket a PostgreSQL. Cuando algo va mal, el diagnóstico consiste en bajar por estas capas hasta encontrar en cuál se rompió la traducción.
Un matiz que ahorra confusión: Spring Data JPA no sustituye a Hibernate, se apoya en él. Y Spring Data JPA no es Spring Data JDBC; comparten familia y el estilo de repositorios, pero por debajo son proyectos distintos (lo veremos en el apartado 8).
- Qué aporta
spring-boot-starter-data-jpa
spring-boot-starter-data-jpaComo todo starter (02-06), es un POM sin código que arrastra un conjunto coherente de dependencias con versiones ya compatibles entre sí.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>Esa única línea añade al classpath:
| Dependencia | Para qué sirve |
|---|---|
spring-boot-starter-jdbc |
DataSource, JdbcTemplate y el pool HikariCP |
hibernate-core |
El motor ORM: implementación de JPA |
jakarta.persistence-api |
Las anotaciones y las interfaces de la especificación |
spring-data-jpa |
Los repositorios y su generación automática |
spring-orm |
La integración de Spring con JPA y su gestor de transacciones |
spring-aspects |
Soporte AOP, base de @Transactional (04-07) |
jakarta.transaction-api |
La API de transacciones |
Lo que no incluye —deliberadamente— es el driver de la base de datos, porque el starter no puede saber a qué motor te vas a conectar. Añadir H2 o PostgreSQL es lo primero que haremos en 04-02.
Puedes comprobar el árbol real en tu proyecto:
EntityManager, unidad de persistencia y contexto de persistencia
EntityManager, unidad de persistencia y contexto de persistenciaEstos tres términos son el vocabulario básico de JPA. Confundirlos es la causa de la mitad de las preguntas de foro sobre «por qué no se guardan mis cambios».
Unidad de persistencia (persistence unit): la configuración completa de un almacén de datos —qué DataSource usar, qué clases son entidades, qué dialecto SQL, qué propiedades de Hibernate—. En una aplicación JPA clásica se declaraba en persistence.xml. En Spring Boot ese fichero no existe: la unidad de persistencia se construye a partir de application.yml y del escaneo automático de entidades. Es una de las cosas que el starter hace por ti.
EntityManager: la interfaz central de JPA. Es tu puerta de entrada a la persistencia. Sus operaciones esenciales:
| Método | Qué hace |
|---|---|
persist(entidad) |
Marca una entidad nueva para ser insertada |
find(Clase, id) |
Recupera por clave primaria (mira antes en el contexto) |
getReference(Clase, id) |
Devuelve un proxy perezoso sin ir a la base de datos |
merge(entidad) |
Reincorpora una entidad separada al contexto |
remove(entidad) |
Marca una entidad gestionada para borrado |
flush() |
Fuerza el envío del SQL pendiente a la base de datos |
detach(entidad) / clear() |
Saca una entidad, o todas, del contexto |
createQuery(jpql) |
Crea una consulta JPQL |
Con Spring Data JPA casi nunca lo usarás directamente, porque los repositorios lo encapsulan. Pero está ahí debajo, y entender su comportamiento explica el de los repositorios.
Contexto de persistencia (persistence context): es el concepto que de verdad hay que interiorizar. Es una caché de primer nivel que el EntityManager mantiene con todas las entidades que ha cargado o guardado durante una unidad de trabajo. Tiene tres propiedades que lo cambian todo:
- Garantiza identidad única: dentro del mismo contexto, dos búsquedas de la estación 1 devuelven exactamente el mismo objeto Java.
estacionA == estacionBestrue. - Detecta cambios automáticamente (dirty checking): guarda una copia del estado original; al hacer flush compara y genera un
UPDATEsolo si algo cambió. Por eso, sobre una entidad gestionada, no hace falta llamar asave()(lo desarrollaremos en 04-07). - Retrasa la escritura (write-behind): acumula el SQL y lo envía en el último momento, lo que permite agruparlo y ordenarlo.
En una aplicación Spring típica, el contexto de persistencia vive lo que dura la transacción, normalmente un método @Transactional del servicio. Fuera de él, las entidades quedan separadas. Ese vínculo entre contexto y transacción es el eje del módulo y el motivo de la lección 04-07.
- Los cuatro estados de una entidad
Toda instancia de una clase @Entity está siempre en uno de estos cuatro estados. Saber en cuál está explica por qué un cambio se guarda o se pierde.
| Estado | ¿Tiene id? | ¿Está en el contexto? | ¿Se sincroniza con la BD? |
|---|---|---|---|
| Transitorio (transient/new) | Normalmente no | No | No |
| Gestionado (managed/persistent) | Sí | Sí | Sí, automáticamente |
| Separado (detached) | Sí | No | No |
| Eliminado (removed) | Sí | Sí, marcada para borrado | Se borrará al hacer flush |
stateDiagram-v2
[*] --> Transitorio: new Estacion()
Transitorio --> Gestionado: persist() / save()
Gestionado --> Separado: fin de transacción / detach() / clear()
Separado --> Gestionado: merge()
Gestionado --> Eliminado: remove() / delete()
Eliminado --> Gestionado: persist()
Eliminado --> [*]: commit (DELETE)
Separado --> [*]: recolector de basura
Recorramos los cuatro con una estación de Ribalta:
// 1. TRANSITORIO: un objeto Java normal y corriente.
Estacion estacion = new Estacion("Plaza Mayor", "Calle Mayor 1", 24);
// Hibernate no sabe que existe. Si la JVM termina, se pierde.
// 2. GESTIONADO: a partir de save(), el contexto lo vigila.
Estacion gestionada = estacionRepositorio.save(estacion);
gestionada.setCapacidad(26);
// NO hace falta volver a llamar a save(): el dirty checking
// detectará el cambio y emitirá el UPDATE al hacer commit.
// 3. SEPARADO: al terminar la transacción, el contexto se cierra.
// A partir de aquí, los cambios ya no se propagan solos:
gestionada.setCapacidad(30); // este cambio NO llega a la base de datos
// 4. Para reincorporarla hay que fusionarla:
Estacion reGestionada = estacionRepositorio.save(gestionada); // hace mergeDos consecuencias que sorprenden a todo el mundo la primera vez:
- El paso 2 no necesita
save()explícito. Muchísimo código Spring lo llama «por si acaso»; es inofensivo pero revela que no se ha entendido el contexto de persistencia. - El paso 3 sí lo necesita, y ahí
save()no inserta: hace merge. La diferencia se detalla en 04-05.
El estado separado es el que produce la temida LazyInitializationException: intentar navegar una asociación perezosa de una entidad cuyo contexto ya se cerró. Lo veremos en 04-04.
- Qué genera Spring Boot al detectar el starter
En cuanto spring-boot-starter-data-jpa está en el classpath y hay un DataSource configurado, la autoconfiguración de 02-06 se dispara. Las clases implicadas son DataSourceAutoConfiguration, HibernateJpaAutoConfiguration y JpaRepositoriesAutoConfiguration, y entre todas registran estos beans sin que escribas una línea:
| Bean creado | Qué hace | Condición |
|---|---|---|
DataSource (HikariCP) |
Pool de conexiones | Hay spring.datasource.url o una BD embebida |
EntityManagerFactory |
Fábrica del EntityManager; construye la unidad de persistencia |
Hay DataSource + Hibernate |
EntityManager (proxy por transacción) |
La puerta de entrada a JPA | Hay EntityManagerFactory |
JpaTransactionManager |
Gestor de transacciones para @Transactional |
Hay EntityManagerFactory |
JpaVendorAdapter |
Adapta Hibernate a la abstracción de Spring | Hibernate en el classpath |
| Repositorios | Un proxy por cada interfaz que extienda Repository |
@EnableJpaRepositories implícito |
PlatformTransactionManager |
Base de la gestión transaccional (04-07) | — |
Además, escanea automáticamente las clases @Entity desde el paquete de la clase anotada con @SpringBootApplication hacia abajo. Como CicloUrbanaApplication vive en com.ciclourbana, todas las entidades de com.ciclourbana.estaciones, .bicicletas y .alquileres se detectan solas. Es la misma regla del escaneo de componentes de 01-04, aplicada a entidades.
Puedes verificarlo con el informe de autoconfiguración de 02-06:
En la sección Positive matches aparecerán entradas como:
HibernateJpaAutoConfiguration matched:
- @ConditionalOnClass found required classes 'jakarta.persistence.EntityManager',
'org.hibernate.SessionFactory' (OnClassCondition)
- @ConditionalOnBean found bean 'dataSource' (OnBeanCondition)
JpaRepositoriesAutoConfiguration#jpaRepositoriesFactoryBean matched:
- @ConditionalOnBean found bean 'entityManagerFactory'Y si no configuras un DataSource teniendo el starter, el arranque falla con un mensaje muy característico:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded
datasource could be configured.
Reason: Failed to determine a suitable driver classEs exactamente el problema que resuelve la lección siguiente.
- Alternativas a JPA en el ecosistema Spring
JPA es la opción por defecto, no la única ni siempre la mejor. Conocer el mapa evita el error de forzarla donde no encaja.
| Tecnología | Nivel de abstracción | Puntos fuertes | Puntos débiles | Cuándo elegirla |
|---|---|---|---|---|
| JdbcTemplate | Bajo: tú escribes el SQL | Control total, sin sorpresas, sin estado | Mapeo manual, código repetitivo | Informes, consultas complejas, cargas masivas |
| Spring Data JDBC | Medio | Modelo simple, sin caché ni carga perezosa, muy predecible | Sin relaciones perezosas ni herencia | Modelos DDD con agregados bien delimitados |
| Spring Data JPA | Alto | Productividad, relaciones, caché, portabilidad | Curva de aprendizaje, comportamiento implícito | CRUD de dominio rico, el caso general |
| jOOQ | Bajo, con SQL tipado | SQL comprobado en compilación, perfecto para consultas complejas | Requiere generación de código; licencia comercial en BD propietarias | Aplicaciones donde el SQL es el centro |
| MyBatis | Bajo-medio | SQL en XML o anotaciones, mapeo flexible | Más configuración, menos automatismo | Migración de aplicaciones con SQL heredado |
| R2DBC | Medio | Acceso reactivo no bloqueante | Ecosistema menor, sin JPA posible | Aplicaciones WebFlux con mucha concurrencia |
Un criterio práctico y honesto: JPA brilla cuando escribes y lees agregados de dominio; se estorba cuando haces informes. En CicloUrbana usaremos Spring Data JPA para todo el CRUD de estaciones, bicicletas y alquileres, y en 04-06 veremos consultas nativas para casos como «estaciones más cercanas», donde el SQL específico de PostgreSQL gana por goleada. No son excluyentes: conviven en el mismo proyecto sobre el mismo DataSource.
- El modelo de datos objetivo de CicloUrbana
Este es el destino del módulo. Seis tablas que reflejan el negocio de la red de Ribalta:
erDiagram
ESTACIONES ||--o{ BICICLETAS : "alberga"
ESTACIONES ||--o{ ALQUILERES : "origen"
ESTACIONES ||--o{ ALQUILERES : "destino"
BICICLETAS ||--o{ ALQUILERES : "es alquilada en"
BICICLETAS ||--o{ INCIDENCIAS : "acumula"
USUARIOS ||--o{ ALQUILERES : "realiza"
TARIFAS ||--o{ ALQUILERES : "se aplica en"
ESTACIONES {
bigint id PK
varchar nombre UK
varchar direccion
int capacidad
numeric latitud
numeric longitud
boolean activa
bigint version
}
BICICLETAS {
bigint id PK
varchar matricula UK
varchar estado
int nivel_bateria
bigint estacion_id FK
bigint version
}
USUARIOS {
bigint id PK
varchar correo UK
varchar nombre
varchar tipo_tarifa
date fecha_alta
}
ALQUILERES {
bigint id PK
bigint usuario_id FK
bigint bicicleta_id FK
bigint estacion_origen_id FK
bigint estacion_destino_id FK
timestamp inicio
timestamp fin
numeric importe
}
TARIFAS {
bigint id PK
varchar codigo UK
numeric precio_minuto
numeric importe_minimo
}
INCIDENCIAS {
bigint id PK
bigint bicicleta_id FK
varchar tipo
varchar descripcion
timestamp reportada_en
}
Fíjate en tres detalles que anticipan lecciones concretas:
- La columna
versionenestacionesybicicletases el bloqueo optimista de 04-03, el que sustituirá alShallowEtagHeaderFilterde 03-03. alquilerestiene dos claves ajenas aestaciones(origen y destino): un caso dondemappedByno basta y hay que nombrar cada@JoinColumnexplícitamente (04-04).importeyprecio_minutosonnumeric, nuncadouble. El porqué está en 04-03, y es una de las reglas menos negociables de todo el módulo.
El recorrido del módulo, apoyado en este modelo:
| Lección | Qué añade a CicloUrbana |
|---|---|
| 04-02 | H2 en desarrollo, PostgreSQL 16 en Docker, HikariCP afinado |
| 04-03 | Estacion, Bicicleta y Alquiler como entidades con @Version y auditoría |
| 04-04 | Las relaciones del diagrama, LAZY y el problema N+1 |
| 04-05 | EstacionRepositorio extends JpaRepository, paginación en la API |
| 04-06 | Consultas derivadas, JPQL, nativas y proyecciones |
| 04-07 | @Transactional en los servicios, propagación y bloqueos |
| 04-08 | Flyway: el esquema como código versionado |
Errores Comunes y Consejos
Creer que JPA es Hibernate. Son cosas distintas: JPA es la especificación, Hibernate la implementación. Importa cuando aparece una anotación como @BatchSize, que es de Hibernate, no de JPA: funciona, pero te ata al proveedor. Comprueba siempre el paquete del import: jakarta.persistence.* es estándar; org.hibernate.annotations.* es específico.
Pensar que un ORM te libra de saber SQL. Es exactamente al revés: cuanto más automática es la generación de SQL, más importante es saber leerlo. Activa el log de sentencias desde el primer día (04-02) y acostúmbrate a mirarlo.
Llamar a save() sobre una entidad ya gestionada. No hace daño, pero indica que no se ha entendido el dirty checking. Dentro de un método @Transactional, modificar una entidad cargada basta.
Olvidar que el contexto de persistencia muere con la transacción. Es el origen de la LazyInitializationException y de los «cambios que no se guardan». Cuando algo raro pase, la primera pregunta debe ser: ¿en qué estado está esta entidad?
Elegir JPA para todo, incluidos los informes. Una consulta de agregación sobre un millón de alquileres no debería pasar por el mapeo de entidades. JdbcTemplate o una consulta nativa con proyección es la respuesta correcta (04-06).
Consejo: dibuja el modelo relacional antes de anotar clases. El diagrama ER de arriba se hizo antes que las entidades, no después. Anotar sin un modelo pensado produce esquemas que hay que rehacer, y en 04-08 verás que rehacer un esquema en producción es caro.
Consejo: conserva la interfaz EstacionRepositorio. El módulo entero es la demostración de que aislar el almacenamiento tras una interfaz permite cambiar de tecnología sin tocar servicios ni controladores.
Ejercicios
Ejercicio 1: clasificar el estado de las entidades
Dado el siguiente método de un servicio de CicloUrbana, indica en qué estado está la variable estacion en cada punto marcado y si el cambio de la línea final llega a la base de datos. Justifícalo.
@Transactional
public void renombrarEstacion(Long id, String nuevoNombre) {
Estacion estacion = new Estacion(); // (A)
estacion = estacionRepositorio.findById(id) // (B)
.orElseThrow(() -> new RecursoNoEncontradoException("Estación", id));
estacion.setNombre(nuevoNombre); // (C)
} // (D) fin del métodoEjercicio 2: elegir la tecnología de acceso a datos
Para cada necesidad del ayuntamiento de Ribalta, elige entre JdbcTemplate, Spring Data JPA, jOOQ o R2DBC, y razona la elección en una frase.
- CRUD completo de estaciones con validación y relaciones con bicicletas.
- Un informe mensual que cruza alquileres, usuarios y tarifas con seis
JOIN, tres subconsultas y funciones de ventana. - Cargar de golpe 500 000 lecturas de sensores de las bicicletas cada noche.
- Un panel en tiempo real que mantiene 20 000 conexiones abiertas mostrando la ocupación de las estaciones.
Ejercicio 3: recorrer las capas
Explica, capa por capa, qué ocurre cuando EstacionService ejecuta estacionRepositorio.findById(3L) y la estación «Parque del Río» no está aún en el contexto de persistencia. Nombra las cinco capas implicadas y qué hace cada una.
Soluciones
Solución 1.
- (A) Transitorio.
new Estacion()crea un objeto Java normal; Hibernate no lo conoce, no tiene id y nada lo vigila. Además, esta línea es inútil: la referencia se sobrescribe inmediatamente. Es un olor a código, no un error. - (B) Gestionado.
findByIdcarga la fila y la incorpora al contexto de persistencia de la transacción abierta por@Transactional. Hibernate guarda una copia de su estado original para el dirty checking. - (C) Gestionado y «sucio». El objeto sigue gestionado; su nombre difiere ahora de la copia original. Todavía no se ha ejecutado ningún
UPDATE. - (D) El cambio SÍ llega a la base de datos. Al terminar el método, el proxy transaccional hace commit; antes del commit se ejecuta un flush que compara estado actual y copia original, detecta el nombre modificado y emite
UPDATE estaciones SET nombre = ? WHERE id = ?. Tras el commit el contexto se cierra y la entidad pasa a separada.
No hace falta llamar a save(). Es el ejemplo canónico de dirty checking. Si el método no llevara @Transactional, el contexto viviría solo lo que dura la llamada al repositorio, la entidad volvería separada y el cambio se perdería en silencio: el peor tipo de fallo, porque no lanza ninguna excepción.
Solución 2.
- Spring Data JPA. Es su caso ideal: agregado de dominio, relaciones, validación, escritura y lectura por identificador. La productividad compensa de sobra la abstracción.
- jOOQ (o
JdbcTemplatecon SQL nativo). SeisJOIN, subconsultas y funciones de ventana no son expresables cómodamente en JPQL, y forzarlo produce consultas ilegibles. jOOQ da SQL tipado y comprobado en compilación;JdbcTemplatees la alternativa sin dependencias añadidas. JdbcTemplateconbatchUpdate. Una carga masiva no debe pasar por el contexto de persistencia: 500 000 entidades gestionadas agotarían la memoria y el dirty checking haría un trabajo inútil. Inserción por lotes en JDBC directo.- R2DBC con WebFlux. Veinte mil conexiones concurrentes con el modelo bloqueante de JDBC exigirían veinte mil hilos. El acceso reactivo no bloqueante está pensado exactamente para esto. (Con Java 21 los hilos virtuales cambian parte de este cálculo, pero R2DBC sigue siendo la opción de manual.)
Solución 3.
EstacionServicellama aestacionRepositorio.findById(3L)sobre la interfaz. No sabe nada de JPA.- El proxy de Spring Data JPA —creado en el arranque, como veremos en 04-05— recibe la llamada y la delega en
SimpleJpaRepository, la implementación estándar. SimpleJpaRepositorytraduce aentityManager.find(Estacion.class, 3L)y envuelve el resultado enOptional.- El
EntityManager(Hibernate) busca primero en el contexto de persistencia. Como no está, genera el SQL adaptado al dialecto:select e1_0.id, e1_0.capacidad, ... from estaciones e1_0 where e1_0.id=?. - JDBC y el driver de PostgreSQL piden una conexión al pool HikariCP, envían la sentencia preparada y devuelven un
ResultSet.
De vuelta, Hibernate construye la instancia de Estacion, la registra en el contexto de persistencia con una copia de su estado, y la devuelve envuelta en Optional. Si el servicio volviera a pedir la estación 3 en la misma transacción, el paso 5 no se repetiría: la caché de primer nivel devolvería el mismo objeto.
Conclusión
Ya tienes el mapa completo del terreno. Sabes que el desfase objeto-relacional no es un capricho sino cinco desencuentros concretos entre dos modelos igual de válidos, y que un ORM existe para absorberlos a cambio de introducir reglas propias. Distingues con precisión las cuatro piezas que se confunden a diario: JDBC como API de bajo nivel, JPA como especificación, Hibernate como implementación que genera el SQL real, y Spring Data JPA como capa que fabrica repositorios a partir de interfaces. Conoces las siete dependencias que arrastra spring-boot-starter-data-jpa y, sobre todo, la que no arrastra: el driver. Entiendes qué es el contexto de persistencia y sus tres propiedades —identidad única, dirty checking y escritura diferida—, que explican por qué modificar una entidad gestionada basta para guardarla. Puedes situar cualquier instancia en uno de los cuatro estados y predecir si sus cambios sobrevivirán. Sabes qué beans crea la autoconfiguración en cuanto detecta el starter y cómo verificarlo con --debug. Y tienes criterio para saber cuándo JPA no es la respuesta y conviene JdbcTemplate, jOOQ o R2DBC.
También tienes el destino a la vista: las seis tablas del modelo de CicloUrbana, con su columna version para el bloqueo optimista, sus dos claves ajenas de alquileres a estaciones y sus importes en numeric.
Falta lo primero de todo: no hay ninguna base de datos. Si añades el starter ahora mismo y arrancas, la aplicación fallará con «Failed to configure a DataSource». La lección 04-02, Configuración de Fuentes de Datos, resuelve eso: montaremos H2 en memoria para desarrollo con su consola web, levantaremos PostgreSQL 16 con un docker-compose.yml propio de CicloUrbana, afinaremos el pool HikariCP parámetro a parámetro con criterios de dimensionado reales, decidiremos qué valor de ddl-auto usar en cada entorno y por qué open-in-view debe desactivarse desde el primer día, y dejaremos el log de SQL configurado para poder ver exactamente qué envía Hibernate a Ribalta.
Curso de Spring Boot
Módulo 1: Introducción a Spring Boot
- ¿Qué es Spring Boot?
- Configuración de tu Entorno de Desarrollo
- Creando tu Primera Aplicación Spring Boot
- Entendiendo la Estructura del Proyecto
- El Arranque y el Ciclo de Vida de la Aplicación
Módulo 2: Conceptos Básicos de Spring Boot
- Anotaciones de Spring Boot
- Inyección de Dependencias en Spring Boot
- Ámbito y Ciclo de Vida de los Beans
- Configuración de Spring Boot
- Propiedades de Spring Boot
- Autoconfiguración y Starters por Dentro
Módulo 3: Construyendo Servicios Web RESTful
- Introducción a los Servicios Web RESTful
- Creando Controladores REST
- Manejo de Métodos HTTP
- Validación de Datos de Entrada
- DTOs y Mapeo entre Capas
- Manejo de Excepciones en REST
- Documentar la API con OpenAPI
Módulo 4: Acceso a Datos con Spring Boot
- Introducción a Spring Data JPA
- Configuración de Fuentes de Datos
- Creación de Entidades JPA
- Relaciones entre Entidades
- Uso de Repositorios de Spring Data
- Métodos de Consulta en Spring Data JPA
- Transacciones y Gestión de la Persistencia
- Migraciones de Esquema con Flyway
Módulo 5: Seguridad en Spring Boot
- Introducción a Spring Security
- Configuración de Spring Security
- Autenticación y Autorización de Usuarios
- Implementación de Autenticación JWT
- Seguridad a Nivel de Método y Endurecimiento de la API
Módulo 6: Pruebas en Spring Boot
- Introducción a las Pruebas
- Pruebas Unitarias con JUnit
- Simulación con Mockito
- Pruebas de Integración
- Pruebas con Testcontainers
Módulo 7: Funciones Avanzadas de Spring Boot
- Spring Boot Actuator
- Perfiles de Spring Boot
- Tareas Programadas y Ejecución Asíncrona
- Spring Boot con Docker
- Spring Boot y Microservicios
- Comunicación entre Servicios y Tolerancia a Fallos
Módulo 8: Despliegue de Aplicaciones Spring Boot
- Introducción al Despliegue
- Desplegando en Heroku
- Desplegando en AWS
- Desplegando en Kubernetes
- Integración y Entrega Continua
Módulo 9: Rendimiento y Monitoreo
- Ajuste de Rendimiento
- Caché con Spring Cache
- Monitoreo con Spring Boot Actuator
- Uso de Prometheus y Grafana
- Gestión de Registros y Logs
- Trazabilidad Distribuida
