← Persistencia con JPA y PostgreSQL

UD5 · Persistir

Lo que debes recordar

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:

El orden de diseño en persistencia
  1. Diseña primero la tabla física en PostgreSQL (tipos, índices, restricciones y claves foráneas).
  2. Mapea la entidad JPA (constructor vacío, @Id Long y columnas explícitas).
  3. Declara el repositorio extendiendo JpaRepository con nombres derivados precisos.
  4. Comprueba el acceso con tests de repositorio (@DataJpaTest, flush() y clear()).
  5. Modela las relaciones con @ManyToOne(fetch = FetchType.LAZY) y lados propietarios claros.
  6. Delimita la atomicidad en el servicio con @Transactional(rollbackFor = Exception.class).
  7. 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

  1. ¿Qué es el desajuste de impedancia (Impedance Mismatch) y en qué cuatro aspectos se manifiesta?
  2. ¿Por qué una base de datos relacional seria exige un pool de conexiones como HikariCP en lugar de abrir conexiones a mano?
  3. ¿Qué peligro tiene utilizar ddl-auto=create-drop o update en un entorno de producción?
  4. ¿Cuáles son los cuatro estados del ciclo de vida de una entidad en JPA?
  5. ¿Qué ocurre si modificas un campo con un setter dentro de un método @Transactional y olvidas llamar a save()?
  6. ¿Por qué una clave foránea en PostgreSQL genera un error al intentar borrar una fila padre que tiene hijos asociados?
  7. ¿Qué diferencia conceptual hay entre borrado físico (Hard Delete) y borrado lógico (Soft Delete)?
  8. ¿Cómo traduce Spring Data un método llamado findByTituloContainingIgnoreCase(String texto) a SQL?
  9. ¿En qué momento valida Spring Data si los métodos declarados en tu interfaz de repositorio tienen nombres coherentes?
  10. ¿Por qué un test de repositorio con mocks no demuestra que tus consultas SQL funcionan?
  11. ¿Qué componentes de Spring carga @DataJpaTest y cuáles ignora para ser tan rápido?
  12. ¿Por qué es obligatorio llamar a clear() en un test antes de ejecutar la consulta que quieres verificar?
  13. ¿Qué significa «lado propietario» de una relación y en qué tabla reside la clave foránea física?
  14. ¿Qué es un Proxy de Hibernate y qué excepción lanza si intentas acceder a sus datos con la sesión cerrada?
  15. ¿Qué significa el parámetro mappedBy en una anotación @OneToMany?
  16. ¿Por qué los métodos helper (como agregarTarea) son obligatorios en relaciones bidireccionales?
  17. ¿Por qué una relación @ManyToMany requiere físicamente una tabla puente en PostgreSQL?
  18. ¿Cuáles son las cuatro garantías ACID y qué significa que una operación sea atómica?
  19. ¿Qué es el problema de la autoinvocación (Self-Invocation) en métodos anotados con @Transactional?
  20. ¿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 @Transactional y 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 @ManyToOne y @ManyToMany usan FetchType.LAZY y Set respectivamente.
  • No se produce ningún StackOverflowError por serialización recursiva ni fugas de Proxies no inicializados.
  • Las consultas de listado están optimizadas con JOIN FETCH o Pageable, 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.