Día 14 de 30 Fase 3: El Salto a Spring Boot 3 y APIs REST

Spring Data JPA e Hibernate: Automatizando la Persistencia

Entidades, Ciclo de Vida del EntityManager, Dirty Checking y Transacciones en 17/21

Para dominar. Al terminar deberías poder hacerlo sin ayuda.

  • 4h de estudio
  • 5 conceptos
  • 3 preguntas
  • 1 katas
Java 17 LTSJava 21 LTSJPAHibernateEntityManagerDirty Checking@Transactional
Al terminar este día sabrás 4
  • Identificar en qué estado está una entidad y qué hará Hibernate con ella.
  • Aprovechar el dirty checking sin llamadas a save() innecesarias.
  • Explicar cómo funciona @Transactional por dentro y sus trampas.
  • Gestionar el esquema con migraciones versionadas en lugar de ddl-auto.

Los 4 Estados de una Entidad en el EntityManager

Transient (nueva en memoria con new), Managed (vigilada por la sesión activa de Hibernate), Detached (transacción cerrada; cambios ya no se sincronizan) y Removed (marcada para DELETE).

En una línea Al llamar a repo.findById(id), la entidad entra directamente al estado MANAGED.

Los cuatro estados en una transacción java
@Transactional
public void ciclo() {
    var tarea = new Tarea("Leer JEP 444");     // NUEVA: Hibernate no la conoce
    em.persist(tarea);                          // GESTIONADA: se insertará en el flush

    Tarea a = em.find(Tarea.class, 7L);         // GESTIONADA: SELECT
    Tarea b = em.find(Tarea.class, 7L);         // misma instancia, sin SELECT
    System.out.println(a == b);                 // true: identidad garantizada

    em.remove(a);                               // ELIMINADA: DELETE en el flush
}                                               // commit -> flush -> el contexto se cierra

public void fuera() {
    Tarea t = repo.findById(7L).orElseThrow();  // la transacción de findById ya terminó
    t.renombrar("Nuevo título");                // SEPARADA: este cambio NO se guarda
}
Cómo funciona por dentro

El contexto de persistencia es un mapa de entidades gestionadas que vive mientras dura la transacción (en Spring, por defecto, una por transacción). Una entidad puede estar en cuatro estados respecto a él.

  • Nueva (transient): creada con new, Hibernate no sabe que existe.
  • Gestionada (managed): está en el contexto, ya sea porque la cargaste con find o una consulta, o porque llamaste a persist. Hibernate vigila sus cambios.
  • Separada (detached): estuvo gestionada pero el contexto se cerró. Tiene id, pero sus cambios ya no se sincronizan.
  • Eliminada (removed): marcada con remove; se borrará en el próximo flush.

El contexto también garantiza identidad: dentro de una transacción, cargar dos veces la misma fila devuelve exactamente el mismo objeto Java y la segunda vez no hay consulta. Por eso se le llama caché de primer nivel.

Es la base de todo el funcionamiento del ORM; sin entender los estados, se producen fallos inexplicables de actualización.

Se conecta con HibernateEntityManagerDirty Checking

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. Bloque 1: Entidades y Repositorios (0 - 25 min)

    Qué estudiar. Anotaciones @Entity, @Id, @GeneratedValue(strategy = IDENTITY), @Table.

    Pregunta a responder. ¿Por qué el constructor sin argumentos de la entidad puede ser protected?

    Resultado esperado. Entidad TareaEntity bien encapsulada con repositorio JpaRepository.

  2. Bloque 2: Comprobación de Dirty Checking (25 - 50 min)

    Qué estudiar. Ciclo de vida Managed y funcionamiento del snapshot del EntityManager.

    Pregunta a responder. ¿Cómo verificar en los logs SQL que Hibernate emite el UPDATE sin llamar a save()?

    Resultado esperado. Test de integración comprobando que la modificación persiste sin llamar a save().

  3. Bloque 3: Trampas de Proxies de @Transactional (50 - 80 min)

    Qué estudiar. Proxies de Spring, rollback automático y llamadas internas.

    Pregunta a responder. ¿Cómo separar métodos transaccionales en servicios independientes para evitar auto-invocación?

    Resultado esperado. Demostración de rollback con excepción no comprobada.

  4. Cierre y Consolidación (80 - 90 min)

    Qué estudiar. Repaso activo de los 4 estados de la entidad.

    Pregunta a responder. Definir qué significa que una entidad esté Detached.

    Resultado esperado. Notas de estudio registradas en la base de datos local.

Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.

Explícalo desde cero

Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.

Por qué no hace falta llamar a repo.save() al modificar una entidad cargada

Lo que debes entender

El EntityManager funciona como una memoria caché de primer nivel con una cámara fotográfica. Al leer la entidad le saca una "foto" (snapshot). Si modificas un campo dentro de @Transactional, al terminar el método compara la foto con el objeto real: si ve cambios ("suciedad" o dirty), escribe el UPDATE SQL en la BD.

Analogía para contarlo

Un editor de documentos en la nube con autoguardado: mientras escribes en el documento abierto (Managed), al cerrar la pestaña se guardan los cambios automáticamente sin necesidad de pulsar un botón de guardar.

Lo que suele explicarse mal

Enseñar JPA como si fuera un repositorio CRUD simple donde siempre hay que llamar a save() para todo.

Señal de que aún no lo dominas

Si ves código con repo.save(tarea) justo después de tarea.setEstado("HECHO") dentro de un método transaccional.

Preguntas trampa

Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.

Nivel 1 (Básico) ¿Por qué las entidades JPA requieren obligatoriamente un constructor sin argumentos (no-args constructor)? Ver respuesta

Respuesta razonada

Porque Hibernate crea las entidades por reflexión: primero instancia un objeto vacío con el constructor sin argumentos y después asigna cada campo con los valores leídos del ResultSet. No puede saber qué constructor con parámetros usar ni en qué orden. También lo necesita para generar los proxies de carga perezosa, que son subclases de tu entidad.

No tiene por qué ser público: la especificación permite protected (y Hibernate acepta incluso package-private), lo que impide que tu código lo use por error y cree entidades en estado inválido.

Qué está evaluando la pregunta

Porque Hibernate utiliza reflexión para instanciar la clase vacía al recuperar filas de la base de datos antes de poblar sus campos mediante getters o reflection directa.

Nivel 2 (Intermedio) ¿Por qué por defecto @Transactional NO hace rollback si se lanza una excepción comprobada (checked exception como IOException)? Ver respuesta

Respuesta razonada

Es una convención heredada de EJB: se considera que una excepción comprobada representa un resultado de negocio previsto (por ejemplo, «saldo insuficiente» modelado como checked) y no un fallo del sistema, así que la transacción hace commit de lo que se hizo hasta ese punto. Las RuntimeException y los Error se consideran fallos inesperados y provocan rollback.

Es una fuente clásica de bugs: lanzas una IOException, crees que se deshizo todo y en la base de datos han quedado datos a medias. Se cambia con @Transactional(rollbackFor = Exception.class) o, mejor, usando excepciones no comprobadas en la lógica de negocio, que es lo que recomienda el propio ecosistema Spring.

Qué está evaluando la pregunta

Por diseño histórico de Spring, el rollback automático solo se dispara ante excepciones no comprobadas (RuntimeException o Error). Para incluir comprobadas se debe especificar @Transactional(rollbackFor = Exception.class).

Nivel 3 (Entrevista Senior) ¿Qué es el problema de la llamada auto-referenciada (self-invocation) en Spring y cómo afecta a @Transactional y @Cacheable? Ver respuesta

Respuesta razonada

@Transactional, @Cacheable, @Async y similares funcionan mediante un proxy que envuelve al bean. Si un método del bean llama a otro método del mismo bean con this.otroMetodo(), la llamada no pasa por el proxy y la anotación se ignora en silencio: no hay transacción nueva, ni caché, ni ejecución asíncrona.

Soluciones, de mejor a peor: mover el método anotado a otro bean (suele indicar que tenía otra responsabilidad), usar TransactionTemplate para controlar la transacción de forma programática, inyectar el propio bean con @Lazy y llamar a través de él, o activar el tejido de AspectJ en compilación, que modifica el bytecode y sí intercepta llamadas internas.

Qué está evaluando la pregunta

Si la ClaseA tiene el método1() y método2() con @Transactional, y método1() llama a this.método2(), la llamada se ejecuta directamente en la instancia de la JVM sin pasar por el proxy de Spring, por lo que las anotaciones de método2() son ignoradas por completo.

Reto del tutor

Un candidato escribe: public TareaEntity actualizar(Long id, String titulo) { var t = repo.findById(id).get(); t.setTitulo(titulo); return repo.save(t); } en un servicio sin @Transactional. Evalúa el código.

Lo que una respuesta suele tener bien

Recupera la entidad, la modifica y la guarda.

Lo que suele quedarse en la superficie

Decir que "falta validar si existe".

El concepto de fondo que suele faltar

No usar @Transactional (lo que hace que la entidad se vuelva Detached fuera de la transacción) y forzar un repo.save() innecesario, además de usar .get() ciegamente.

Cómo lo respondería alguien senior en una entrevista

El código tiene 3 fallos graves: (1) Usar findById().get() lanza NoSuchElementException si el ID no existe (debe usarse .orElseThrow()); (2) Al no tener @Transactional, la sesión se abre y cierra en el findById, dejando la entidad Detached y requiriendo un save() que hace un merge innecesario; (3) Con @Transactional, el repo.save(t) sobra por completo gracias al Dirty Checking, ejecutándose de forma atómica y más eficiente.

Proyecto «Pedidos» · paso 14 de 30

Persistencia con Spring Data JPA

Sustituir el JDBC manual por entidades JPA y repositorios de Spring Data.

De qué va el proyecto

El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.

Qué hacer

  1. Mapea Pedido, LineaPedido y Cliente como entidades y crea sus repositorios con JpaRepository.
  2. Marca los métodos de PedidoService que modifican datos con @Transactional.
  3. Mantén los DTOs de la API separados de las entidades.

Está listo cuando

  • Confirmar un pedido cambia su estado en la base de datos sin llamar explícitamente a save().
  • La API no devuelve entidades JPA en ninguna respuesta.

Katas: ejercicios cortos

Reglas de oro

  • En métodos @Transactional, no llames a repo.save() tras modificar una entidad Managed; confía en el Dirty Checking.
  • Las entidades de base de datos no deben ser records; los records son para DTOs.
  • Evita las llamadas auto-referenciadas dentro de la misma clase para métodos con @Transactional.
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown