# Día 14: Spring Data JPA e Hibernate: Automatizando la Persistencia

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

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 14 de 30
- Fase 3: El Salto a Spring Boot 3 y APIs REST
- Dedicación: 4h
- Profundidad: para dominar
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/spring-data-jpa-hibernate/

## Al terminar este día sabrás

- 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`.


## Conceptos

### 1. 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).

**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.

*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
}
```

- **En una línea:** Al llamar a `repo.findById(id)`, la entidad entra directamente al estado MANAGED.
- **Por qué importa:** Es la base de todo el funcionamiento del ORM; sin entender los estados, se producen fallos inexplicables de actualización.
- **Error típico:** Creer que llamar a `repo.save()` es obligatorio para guardar modificaciones en una entidad Managed.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Hibernate, EntityManager, Dirty Checking

### 2. Dirty Checking: Sincronización Automática con la BD

Al finalizar un método `@Transactional`, Hibernate compara el estado actual de todas las entidades Managed contra el snapshot que tomó al cargarlas. Si detecta diferencias, emite el UPDATE SQL automáticamente.

**Cómo funciona por dentro.**

Cuando Hibernate carga una entidad, guarda una copia de sus valores (snapshot). Antes de confirmar la transacción ejecuta un flush: compara cada entidad gestionada con su snapshot y genera un `UPDATE` por cada una que haya cambiado. A esto se le llama dirty checking.

La consecuencia es que dentro de una transacción no hace falta llamar a `save()` para guardar cambios de una entidad cargada: basta con modificarla. Llamar a `save()` no rompe nada, pero si la entidad está separada, Spring Data hace `merge`, que puede lanzar un `SELECT` adicional.

El flush ocurre al hacer commit, antes de ejecutar una consulta JPQL que podría verse afectada por cambios pendientes, o si llamas a `flush()` a mano. Con `@Transactional(readOnly = true)` Hibernate desactiva el dirty checking y ahorra memoria y CPU, porque no necesita snapshots.

*Actualizar sin save()*

```java
@Service
class TareaService {
    private final TareaRepository repo;
    TareaService(TareaRepository repo) { this.repo = repo; }

    @Transactional
    public void completar(long id, Clock reloj) {
        Tarea tarea = repo.findById(id)
            .orElseThrow(() -> new TareaNoEncontradaException(id));
        tarea.completar(reloj);      // solo se modifica el objeto
    }                                // commit: flush detecta el cambio y emite el UPDATE

    @Transactional(readOnly = true)  // sin snapshots ni dirty checking
    public List<TareaResponse> pendientes() {
        return repo.findByCompletadaFalse().stream().map(TareaResponse::desde).toList();
    }
}

// Con spring.jpa.show-sql=true verás:
// select t1_0.id, t1_0.titulo, ... from tarea t1_0 where t1_0.id=?
// update tarea set completada=?, completada_en=?, titulo=? where id=?
```

- **En una línea:** `tarea.actualizarTitulo("Nuevo");` dentro de `@Transactional` actualiza la BD sin llamar a save().
- **Por qué importa:** Ahorra cientos de llamadas redundantes a `repo.save()` y garantiza que solo se modifiquen los campos que realmente cambiaron.
- **Error típico:** Poner `repo.save(entidad)` al final de un método transaccional tras modificar un setter.
- **Java 17 frente a 21:** Optimizado en Hibernate 6 (Spring Boot 3).
- **Se conecta con:** @Transactional, EntityManager, Rendimiento

### 3. Proxies Dinámicos de @Transactional

Spring envuelve tu servicio en un proxy CGLIB. Al entrar abre la transacción de base de datos; al salir sin excepciones hace commit; si ocurre una RuntimeException hace rollback.

**Cómo funciona por dentro.**

Spring no modifica tu clase. Al crear el bean, detecta `@Transactional` y registra en su lugar un proxy que envuelve al objeto real. Cuando otro bean llama a un método, en realidad llama al proxy, que abre la transacción (o se une a una existente), delega en tu objeto y, al volver, hace commit; si salió una excepción no comprobada, hace rollback.

De ahí salen las reglas que hay que conocer: solo funciona en llamadas que atraviesan el proxy (desde otro bean), no en llamadas internas con `this`; los métodos `private` no se interceptan; y la propagación por defecto (`REQUIRED`) hace que un método transaccional llamado desde otro se una a la transacción existente, de modo que una excepción en el interno marca toda la transacción para rollback.

Otras propagaciones útiles: `REQUIRES_NEW` suspende la actual y abre otra independiente (por ejemplo, para registrar una auditoría aunque la operación principal falle).

*Lo que hace el proxy, en pseudocódigo*

```java
// Lo que Spring genera conceptualmente para TareaService
class TareaService$$Proxy extends TareaService {
    private final TareaService objetivo;
    private final PlatformTransactionManager tm;

    @Override
    public void completar(long id, Clock reloj) {
        TransactionStatus tx = tm.getTransaction(new DefaultTransactionDefinition());
        try {
            objetivo.completar(id, reloj);       // tu código
            tm.commit(tx);
        } catch (RuntimeException | Error e) {
            tm.rollback(tx);                     // rollback por defecto
            throw e;
        } catch (Exception checked) {
            tm.commit(tx);                       // ¡las comprobadas hacen COMMIT por defecto!
            throw checked;
        }
    }
}

// Auditoría que debe persistir aunque falle la operación principal
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registrarIntento(String accion) { auditoriaRepo.save(new Auditoria(accion)); }
```

- **En una línea:** `@Transactional public void transferir(...) { ... }`
- **Por qué importa:** Permite desentenderse de la gestión manual de transacciones que sufrimos en el Día 5 con JDBC.
- **Error típico:** Llamar a un método `@Transactional` desde otro método dentro de la misma clase (`this.metodo()`), donde el proxy no interviene y la transacción NO se crea.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** AOP Proxies, Rollback, ACID

### 4. Por qué las Entidades JPA NO pueden ser Records

JPA e Hibernate requieren que las entidades tengan constructor sin argumentos, campos mutables para lazy loading y clases que puedan ser extendidas por proxies dinámicos (los records son final e inmutables).

**Cómo funciona por dentro.**

JPA exige de una entidad cosas que un record no puede ofrecer. Necesita un constructor sin argumentos para instanciarla por reflexión antes de rellenar los campos, y un record solo tiene el constructor canónico. Necesita campos modificables para asignar el id generado y para el dirty checking, y los de un record son `final`. Y para la carga perezosa crea subclases proxy de la entidad, imposible si la clase es `final`, como lo es todo record.

Una entidad bien escrita tiene un constructor protegido sin argumentos para JPA, un constructor público que garantiza un estado válido, métodos de negocio en lugar de setters y un `equals`/`hashCode` estable. Este último es delicado: si usas el id generado, cambia al persistir. Una opción segura es comparar por id solo si no es nulo y usar un `hashCode` constante por clase.

Los records sí brillan alrededor de las entidades: como DTOs, como proyecciones de consultas y como objetos de valor embebibles (`@Embeddable` admite records desde Hibernate 6.2).

*Entidad JPA bien construida*

```java
@Entity
@Table(name = "tarea")
public class Tarea {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 120)
    private String titulo;

    private boolean completada;
    private Instant completadaEn;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)   // LAZY: ManyToOne es EAGER por defecto
    private Proyecto proyecto;

    @Version
    private long version;                                  // bloqueo optimista

    protected Tarea() { }                                  // para JPA, no para ti

    public Tarea(String titulo, Proyecto proyecto) {
        this.titulo = Objects.requireNonNull(titulo);
        this.proyecto = Objects.requireNonNull(proyecto);
    }

    public void completar(Clock reloj) {
        if (completada) throw new IllegalStateException("Ya completada");
        completada = true;
        completadaEn = Instant.now(reloj);
    }

    @Override public boolean equals(Object o) {
        return o instanceof Tarea t && id != null && id.equals(t.id);
    }
    @Override public int hashCode() { return Tarea.class.hashCode(); }   // estable antes y después de persistir
}
```

- **En una línea:** `Clase con atributos privados, constructor protegido sin args y métodos de negocio.`
- **Por qué importa:** Pregunta recurrente en entrevistas: los records son para DTOs; las entidades de base de datos deben ser clases normales.
- **Error típico:** Poner `@Entity` sobre un `public record TareaEntity(...)` y sorprenderse de que falle al compilar.
- **Java 17 frente a 21:** Aplica tanto a Java 17 como a Java 21.
- **Se conecta con:** Records vs Entities, Hibernate Proxies, Arquitectura

### 5. Desactivar ddl-auto=update en Entornos Reales

Dejar que Hibernate altere el esquema en producción es un peligro crítico de pérdida de datos. En producción debe usarse `validate` o `none` junto con Flyway.

**Cómo funciona por dentro.**

`spring.jpa.hibernate.ddl-auto=update` hace que Hibernate compare las entidades con la base de datos al arrancar y añada lo que falta. Es cómodo en un prototipo y peligroso en cualquier otro sitio: nunca borra columnas ni las renombra (un renombrado crea una columna nueva vacía), no migra datos, no deja historial de qué cambió ni cuándo, y dos instancias arrancando a la vez pueden pelearse.

En entornos reales el esquema se gestiona con migraciones versionadas: Flyway o Liquibase. Cada cambio es un fichero SQL con número de versión (`V3__anadir_prioridad.sql`) que se sube al repositorio con el código, se revisa en el pull request y se aplica una sola vez en orden. Flyway guarda en la tabla `flyway_schema_history` qué migraciones se aplicaron y con qué checksum.

Con migraciones, Hibernate debe quedarse en `validate` (comprueba que entidades y esquema coinciden y falla al arrancar si no) o en `none`.

*Flyway en lugar de ddl-auto*

```yaml
# application.yml
spring:
  jpa:
    hibernate:
      ddl-auto: validate          # Hibernate solo comprueba, nunca modifica
    open-in-view: false           # evita consultas perezosas desde la vista/serialización
  flyway:
    enabled: true                 # con flyway-core en el classpath se ejecuta al arrancar

# src/main/resources/db/migration/V1__crear_tarea.sql
#   CREATE TABLE tarea (
#     id            BIGSERIAL PRIMARY KEY,
#     titulo        VARCHAR(120) NOT NULL,
#     completada    BOOLEAN      NOT NULL DEFAULT FALSE,
#     completada_en TIMESTAMPTZ,
#     version       BIGINT       NOT NULL DEFAULT 0
#   );
#
# src/main/resources/db/migration/V2__anadir_prioridad.sql
#   ALTER TABLE tarea ADD COLUMN prioridad SMALLINT NOT NULL DEFAULT 5;
```

- **En una línea:** `spring.jpa.hibernate.ddl-auto=validate` en application.yml.
- **Por qué importa:** Garantiza que la base de datos se modifique mediante scripts SQL auditados y versionados.
- **Error típico:** Perder una columna o tabla en producción porque Hibernate decidió recrear el esquema al arrancar.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Flyway, DevOps, Producción


## Sesión de 90 minutos

### 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`?
- **Entregable:** Entidad `TareaEntity` bien encapsulada con repositorio `JpaRepository`.

### 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()?
- **Entregable:** Test de integración comprobando que la modificación persiste sin llamar a `save()`.

### 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?
- **Entregable:** Demostración de rollback con excepción no comprobada.

### 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.
- **Entregable:** Notas de estudio registradas en la base de datos local.


## Explícalo desde cero

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

- **Qué 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.
- **Qué suele explicarse mal:** Enseñar JPA como si fuera un repositorio CRUD simple donde siempre hay que llamar a save() para todo.
- **Ejemplo sencillo:** 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.
- **Señal de alerta:** Si ves código con `repo.save(tarea)` justo después de `tarea.setEstado("HECHO")` dentro de un método transaccional.


## Preguntas

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

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.

**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.

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

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)`.

**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.

### 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`?

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.

**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.


## Reto del tutor

**Reto:** 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 está bien:** Recupera la entidad, la modifica y la guarda.
- **Respuesta superficial:** Decir que "falta validar si existe".
- **Lo que falta de fondo:** 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.
- **Respuesta senior:** 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.


## Katas

### Kata 14.1: Demostrar Dirty Checking con test de integración

Escribe un método de servicio transaccional que recupere una entidad, modifique un campo sin invocar `repo.save()` y verifique con una nueva consulta en un test `@SpringBootTest` que el cambio persistió en la BD.


## 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`.


## Proyecto guiado «Pedidos»: Persistencia con Spring Data JPA

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

**Tareas:**

- Mapea `Pedido`, `LineaPedido` y `Cliente` como entidades y crea sus repositorios con `JpaRepository`.
- Marca los métodos de `PedidoService` que modifican datos con `@Transactional`.
- 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.


## Fuentes oficiales

- [Spring Data JPA: referencia](https://docs.spring.io/spring-data/jpa/reference/)
- [Documentación de Hibernate ORM](https://hibernate.org/orm/documentation/)

