El método
La UD4 enseñó a estructurar el código en capas desacopladas. Esta unidad ha demostrado por qué esa inversión valió la pena: cambiamos todo el sistema de almacenamiento de memoria a PostgreSQL y el servicio no modificó ni una sola regla de negocio.
Para persistir con JPA sin caer en sus trampas de rendimiento, sigue siempre este orden de ingeniería:
- Diseña primero la tabla física en PostgreSQL (tipos, índices, restricciones y claves foráneas).
- Mapea la entidad JPA (constructor vacío,
@Id Longy columnas explícitas). - Declara el repositorio extendiendo
JpaRepositorycon nombres derivados precisos. - Comprueba el acceso con tests de repositorio (
@DataJpaTest,flush()yclear()). - Modela las relaciones con
@ManyToOne(fetch = FetchType.LAZY)y lados propietarios claros. - Delimita la atomicidad en el servicio con
@Transactional(rollbackFor = Exception.class). - Audita el SQL en consola para erradicar el problema N+1 mediante
JOIN FETCH.
La idea más importante
Un ORM no te exime de saber SQL: te exige entenderlo mejor. Las abstracciones tienen fugas. Si no entiendes cómo traduce Hibernate tus grafos de objetos a sentencias en PostgreSQL, tu backend funcionará en tu portátil y colapsará en producción.
De ahí nacen todas las buenas prácticas: por eso usamos FetchType.LAZY, por eso usamos Set y nunca List en relaciones N:M, por eso comprobamos la existencia antes de borrar, y por eso las entidades jamás se serializan a JSON sin un DTO intermedio.
El desajuste de impedancia está domado, no eliminado
El motor relacional habla de conjuntos, tablas y filas. Java habla de grafos, tipos, encapsulación e identidad en memoria. JPA tiende un puente entre ambos mundos, pero el coste de cruzar ese puente es entender en todo momento qué sentencias viajan por la red.
Las decisiones que tienes que saber justificar
| Decisión | Lo que tienes que poder decir |
|---|---|
Long y no long primitivo para el @Id |
El primitivo nace con valor 0, lo que confunde a Hibernate haciéndole creer que ya existe en la base de datos. El objeto envoltorio nace con valor null, señalando sin ambigüedad que se trata de una entidad nueva y transitoria que requiere un INSERT. |
| Constructor vacío obligatorio | Hibernate necesita instanciar la clase vacía mediante reflexión de Java antes de rellenar sus campos privados con los valores leídos de las columnas de la base de datos. |
Usar siempre la instancia devuelta por save() |
save() sincroniza la entidad con el contexto de persistencia y devuelve la referencia gestionada (managed) con el identificador autoincremental asignado por PostgreSQL y las columnas con valores por defecto. |
| DTOs independientes de las entidades | Desacoplan el contrato público de la API de la estructura física de almacenamiento, evitan exponer secretos o columnas internas y erradican de raíz los ciclos infinitos de serialización (StackOverflowError). |
FetchType.LAZY en relaciones @ManyToOne |
Previene que Hibernate cargue inmediatamente todas las entidades asociadas, evitando el catastrófico problema N+1 de consultas en listados masivos. |
Set y nunca List en relaciones @ManyToMany |
Con Set, Hibernate garantiza unicidad e inserta o borra únicamente la fila modificada en la tabla puente. Con List, se ve obligado a borrar todas las filas de la tabla puente y reinsertarlas una por una. |
Prohibido CascadeType.REMOVE en ManyToMany |
Evita que al eliminar una entidad hija (una tarea) se destruyan por error las entidades maestras compartidas (las etiquetas de todo el sistema). |
orphanRemoval = true en OneToMany |
Garantiza que si un elemento se elimina de la colección gestionada de su entidad padre, Hibernate emita automáticamente un DELETE en PostgreSQL, evitando registros huérfanos. |
flush() y clear() en tests de repositorio |
flush() fuerza la ejecución inmediata del SQL pendiente y clear() vacía la caché de primer nivel de la memoria RAM, impidiendo que el test dé falsos positivos al consultar datos cacheados. |
rollbackFor = Exception.class |
Por defecto Spring solo revierte la transacción ante RuntimeException. Añadir rollbackFor garantiza que excepciones comprobadas no provoquen un COMMIT accidental sobre datos a medio procesar. |
JOIN FETCH en lugar de JOIN en JPQL |
Instruye a Hibernate a realizar el JOIN físico y poblar inmediatamente la entidad relacionada en una única consulta SQL, reduciendo 101 consultas a solo 1. |
Al terminar deberías poder responder
- ¿Qué es el desajuste de impedancia (Impedance Mismatch) y en qué cuatro aspectos se manifiesta?
- ¿Por qué una base de datos relacional seria exige un pool de conexiones como HikariCP en lugar de abrir conexiones a mano?
- ¿Qué peligro tiene utilizar
ddl-auto=create-dropoupdateen un entorno de producción? - ¿Cuáles son los cuatro estados del ciclo de vida de una entidad en JPA?
- ¿Qué ocurre si modificas un campo con un setter dentro de un método
@Transactionaly olvidas llamar asave()? - ¿Por qué una clave foránea en PostgreSQL genera un error al intentar borrar una fila padre que tiene hijos asociados?
- ¿Qué diferencia conceptual hay entre borrado físico (Hard Delete) y borrado lógico (Soft Delete)?
- ¿Cómo traduce Spring Data un método llamado
findByTituloContainingIgnoreCase(String texto)a SQL? - ¿En qué momento valida Spring Data si los métodos declarados en tu interfaz de repositorio tienen nombres coherentes?
- ¿Por qué un test de repositorio con mocks no demuestra que tus consultas SQL funcionan?
- ¿Qué componentes de Spring carga
@DataJpaTesty cuáles ignora para ser tan rápido? - ¿Por qué es obligatorio llamar a
clear()en un test antes de ejecutar la consulta que quieres verificar? - ¿Qué significa «lado propietario» de una relación y en qué tabla reside la clave foránea física?
- ¿Qué es un Proxy de Hibernate y qué excepción lanza si intentas acceder a sus datos con la sesión cerrada?
- ¿Qué significa el parámetro
mappedByen una anotación@OneToMany? - ¿Por qué los métodos helper (como
agregarTarea) son obligatorios en relaciones bidireccionales? - ¿Por qué una relación
@ManyToManyrequiere físicamente una tabla puente en PostgreSQL? - ¿Cuáles son las cuatro garantías ACID y qué significa que una operación sea atómica?
- ¿Qué es el problema de la autoinvocación (Self-Invocation) en métodos anotados con
@Transactional? - ¿Qué es el problema N+1, cómo se diagnostica en la consola de logs y cómo lo elimina la cláusula
JOIN FETCH?
El vocabulario de la unidad
| Concepto | Significa |
|---|---|
| ORM | Mapeo Objeto-Relacional: traduce clases y objetos Java a tablas y filas SQL. |
| JPA | Especificación estándar de Java que define las anotaciones y comportamiento de persistencia. |
| Hibernate | El motor relacional (implementación concreta) que ejecuta las directrices de JPA. |
| HikariCP | El pool de conexiones de alto rendimiento que administra sockets TCP con PostgreSQL. |
| Contexto de persistencia | Zona de memoria en RAM donde Hibernate monitoriza las entidades gestionadas durante una transacción. |
| EntityManager | La interfaz central de JPA responsable de persistir, buscar y sincronizar entidades con la base de datos. |
| Estado Transitorio | Objeto nuevo creado con new, sin identificador en base de datos y no monitorizado por JPA. |
| Estado Gestionado | Entidad asociada activamente al contexto de persistencia; sus cambios se sincronizan automáticamente. |
| Dirty Checking | Comparación automática de Hibernate al final de la transacción que emite sentencias UPDATE para campos alterados. |
| Consultas derivadas | Métodos de Spring Data cuyo nombre define las cláusulas WHERE, operadores y ordenación de la consulta SQL. |
| Slice Testing | Pruebas de integración acotadas (@DataJpaTest) que solo levantan la rebanada de persistencia. |
| TestEntityManager | Herramienta de pruebas para forzar escrituras SQL (flush) y vaciar la memoria intermedia (clear). |
| Lado propietario | La entidad que mapea la clave foránea física en su tabla mediante @JoinColumn. |
mappedBy |
Indica el lado inverso de una relación; delega la escritura física en el campo correspondiente del otro lado. |
FetchType.LAZY |
Carga bajo demanda: no consulta la relación de base de datos hasta que se accede explícitamente a ella. |
| Proxy de Hibernate | Subclase intermedia ligera generada en memoria que sustituye temporalmente a una entidad perezosa. |
orphanRemoval |
Borrado automático en base de datos de entidades hijas cuando se desconectan de la colección de su padre. |
| Tabla puente (Join Table) | Tabla relacional intermedia con dos claves foráneas que permite modelar relaciones N:M. |
| Atomicidad (ACID) | Garantía transaccional de que todas las operaciones se confirman juntas (COMMIT) o se cancelan juntas (ROLLBACK). |
| Problema N+1 | Antipatrón de rendimiento donde consultar N elementos dispara N consultas SQL individuales adicionales. |
JOIN FETCH |
Cláusula JPQL que obliga a Hibernate a traer la entidad y sus dependencias en una única sentencia SQL unificada. |
Comprobación final del producto
Comprobación final · con el proyecto y PostgreSQL delante
- La aplicación arranca conectada a una base de datos PostgreSQL real sin errores de dialecto ni de HikariCP.
- Todas las tablas, columnas y claves foráneas están creadas respetando restricciones de integridad (
NOT NULL,UNIQUE,REFERENCES). - El ciclo CRUD completo funciona desde HTTP, persistiendo datos reales que sobreviven al reinicio del servidor.
- Los métodos del servicio están protegidos por
@Transactionaly reverten de forma demostrable ante excepciones. - Los métodos de consulta derivados y personalizados están verificados mediante tests de repositorio con
@DataJpaTest. - Las relaciones
@ManyToOney@ManyToManyusanFetchType.LAZYySetrespectivamente. - No se produce ningún
StackOverflowErrorpor serialización recursiva ni fugas de Proxies no inicializados. - Las consultas de listado están optimizadas con
JOIN FETCHoPageable, erradicando el problema N+1 de la consola.
Resultados de la unidad
- Explicar qué resuelve un ORM y qué problemas introduce.
- Configurar PostgreSQL y mapear entidades con JPA.
- Implementar un CRUD persistente y consultas derivadas.
- Comprobar el acceso a datos con tests de repositorio.
- Modelar relaciones uno a muchos y muchos a muchos sin romper la integridad.
- Reconocer y corregir el problema N+1.
Ya deberías ser capaz de
- Explicar qué resuelve un ORM y qué problemas introduce.
- Configurar PostgreSQL y mapear entidades con JPA.
- Implementar un CRUD persistente y consultas derivadas.
- Comprobar el acceso a datos con tests de repositorio.
- Modelar relaciones uno a muchos y muchos a muchos sin romper la integridad.
- Reconocer y corregir el problema N+1.