====== 05 - Persistencia: Spring JPA ====== En 1.º DAW la persistencia se implementó con **JDBC**: SQL explícito, ''PreparedStatement'' y mapeo manual de ''ResultSet'' a modelo. **JPA** (*Java Persistence API*) sustituye esa capa por un mapeo objeto-relacional gestionado: las entidades se declaran con anotaciones y el proveedor genera el SQL. **Spring Data JPA** añade la pieza de repositorio: interfaces que derivan su implementación de la firma de sus métodos. ===== 1. De JDBC a JPA ===== ^ Aspecto ^ JDBC ^ JPA ^ | SQL | Escrito a mano para cada operación | Generado a partir del mapeo; consultable con **JPQL** | | Mapeo de filas | Manual (''ResultSet'' → modelo) | Automático entre entidad y tabla | | Identidad | Cada fila es un objeto nuevo sin estado gestionado | El **contexto de persistencia** gestiona el ciclo de vida | | Cambios | ''UPDATE'' explícito | Sincronización automática de entidades modificadas | Con **Hibernate** como proveedor habitual, la aplicación declara el mapeo y delega la ejecución. El **DAO** de 1.º DAW se conserva como idea: sigue existiendo una pieza que accede a datos; cambia el mecanismo que usa por debajo. ===== 2. Entidades ===== Una **entidad** es la representación persistente de un modelo de dominio. Se declara con ''@Entity'' e identifica cada fila con ''@Id'': @Entity @Table(name = "orders") public class OrderEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; @Column(nullable = false) String customerId; @Enumerated(EnumType.STRING) @Column(nullable = false) OrderStatus status; @Embedded AmountEntity amount; } La entidad pertenece a la capa ''persistence''; el modelo enriquecido sigue en ''domain''. Son representaciones distintas del mismo concepto, con roles distintos: la entidad describe cómo se almacena el dato; el modelo, qué reglas lo protegen. Los **value objects** del dominio pueden representarse en la entidad como tipos embebidos (''@Embedded'') que también se mapean a tablas o columnas. ==== 2.1 Relaciones ==== JPA mapea las relaciones del diagrama de clases entre tablas: ^ Relación UML ^ Anotación ^ Tabla resultante ^ | Asociación 1 a muchos | ''@OneToMany'' / ''@ManyToOne'' | Columna clave externa en la tabla del lado «muchos» | | Muchos a muchos | ''@ManyToMany'' | **Tabla intermedia** generada | | Uno a uno | ''@OneToOne'' | Clave externa o clave compartida | @Entity public class OrderEntity { @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) List lines; } En una arquitectura modular, las entidades de un módulo no referencian entidades de otro módulo: entre módulos se relacionan los datos por **identificadores**, igual que los modelos de dominio. Mantener las relaciones dentro del módulo conserva la propiedad de sus tablas. Las relaciones cargadas de forma automática pueden desencadenar consultas adicionales inesperadas (el problema **N+1**: una consulta por cada elemento accedido). Configurar la carga perezosa de forma explícita (''fetch = FetchType.LAZY'') y recuperar la relación en la misma consulta con **''fetch join''** o ''@EntityGraph'' cuando se necesite. ===== 3. DAO y repositorio ===== La capa ''persistence'' compone tres piezas con diferentes responsabilidades sobre los datos: ==== 3.1 La interfaz Spring Data ==== **Spring Data JPA** genera la implementación a partir de una interfaz que extiende ''JpaRepository''. Las consultas se derivan del nombre del método: public interface OrderJpaRepository extends JpaRepository { List findByStatus(OrderStatus status); } Para consultas que no se expresan bien por derivación, **JPQL** consulta entidades, no tablas: @Query("select o from OrderEntity o join fetch o.lines where o.status = :status") List findPendingWithLines(OrderStatus status); ==== 3.2 El DAO sobre la interfaz JPA ==== El DAO oculta Spring Data tras su propia interfaz: sus métodos trabajan con entidades y trasladan los detalles técnicos (paginación, consultas nativas, bloqueo optimista) a un único sitio: public interface OrderDao { Optional findById(long id); List findByStatus(OrderStatus status); } @Named class OrderDaoImpl implements OrderDao { private final OrderJpaRepository jpa; @Inject public OrderDaoImpl(OrderJpaRepository jpa) { this.jpa = jpa; } public Optional findById(long id) { return jpa.findById(id); } } ==== 3.3 El repositorio del dominio y su implementación ==== La arquitectura de referencia mantiene el contrato definido en ''domain'' y su implementación en ''persistence''. El repositorio de Spring Data y el DAO son piezas internas de la persistencia, no el contrato del dominio: // domain public interface OrderRepository { Optional findById(long id); void save(Order order); } // persistence @Named class OrderRepositoryImpl implements OrderRepository { private final OrderDao dao; private final OrderPersistenceMapper mapper; @Inject public OrderRepositoryImpl(OrderDao dao, OrderPersistenceMapper mapper) { this.dao = dao; this.mapper = mapper; } public Optional findById(long id) { return dao.findById(id).map(mapper::toModel); } } La **inversión de dependencias** se mantiene (tema 01): el dominio declara la abstracción; JPA es un detalle de implementación que el dominio no conoce. @startuml allowmixing package "domain" { interface OrderRepository class OrderServiceImpl class Order } package "persistence" { interface "OrderJpaRepository\n(Spring Data)" as Jpa interface OrderDao class OrderDaoImpl class OrderRepositoryImpl class OrderEntity class OrderPersistenceMapper } database "Base de datos" as db OrderServiceImpl --> OrderRepository : utiliza OrderRepositoryImpl ..|> OrderRepository : implementa OrderRepositoryImpl --> OrderDao : utiliza OrderDaoImpl ..|> OrderDao : implementa OrderDaoImpl --> Jpa : utiliza OrderDaoImpl --> OrderEntity : gestiona Jpa --> db @enduml ==== 3.4 El mapeo entidad ↔ modelo con MapStruct ==== El mapeo entre entidad y modelo se declara con **MapStruct** y la implementación se genera en compilación; transforma datos, no aplica reglas. @Mapper public interface OrderPersistenceMapper { Order toModel(OrderEntity entity); OrderEntity toEntity(Order order); } Con MapStruct varias transformaciones pueden compartir el mismo mapper declarando cada mapeo con su origen y destino. La documentación de MapStruct detalla opciones avanzadas (mapeos anidados, fechas, colecciones). ===== 4. Transacciones ===== Una **transacción** agrupa operaciones que deben completarse todas o ninguna, con la propiedad **ACID** del almacén: ''commit'' consolida, ''rollback'' revierte. Con JDBC se gestionaban a mano; en Spring, la anotación ''@Transactional'' delega la gestión: @Named public class CancelOrder { @Transactional public OrderSummaryDto cancel(CancelOrderDto request) { // Si algo falla, todo el cambio se revierte. } } ''@Transactional'' es anotación estándar (''jakarta.transaction''), admisible en la frontera del caso de uso sin arrastrar Spring al dominio. Se aplica en la capa de aplicación o en la persistencia, no se reparte por la mitad de los métodos del dominio puro: delimita la **unidad de trabajo**. Para consultas, ''readOnly = true'' señala el acceso solo de lectura y ayuda al proveedor a optimizar. La anotación es una decisión de composición de la aplicación, no de cada método. Añadirla indiscriminadamente amplía el alcance de las transacciones y puede degradar el rendimiento y crear bloqueos innecesarios. ===== 5. Configuración ===== El esquema y los detalles de la conexión se configuran como propiedades; el esquema lo aplican **migraciones** (Flyway, visto en 1.º DAW) y Hibernate valida el mapeo sin alterar la base: spring.datasource.url=jdbc:mariadb://localhost:3306/app spring.jpa.hibernate.ddl-auto=validate spring.jpa.open-in-view=false ''spring.jpa.open-in-view=false'' desactiva la sesión abierta durante toda la petición (el *open session in view* de Spring Boot, activo por defecto). Sin ese ajuste, las relaciones perezosas se cargan más tarde desde el ''controller'', lo que oculta el coste N+1 y mantiene conexiones abiertas. Con el ajuste aplicado, los datos deben dejarse listados en el caso de uso: la consulta única, con ''fetch join'' o ''@EntityGraph'', deja de necesitar la sesión abierta. ===== 6. Pruebas de persistencia ===== La persistencia se prueba contra una base de datos real: los mocks no comprueban SQL, mapeo ni transacciones. * Probar las operaciones del DAO y del repositorio: guardar y recuperar, consultas derivadas y JPQL, paginación. * Probar el mapeo entidad ↔ modelo, incluyendo relaciones y value objects embebidos. * Probar la transacción: la operación fallida no deja cambios parciales. * No probar aquí las reglas de negocio: pertenecen a las pruebas del modelo, que no necesitan base de datos. @DataJpaTest class OrderPersistenceTest { @Autowired OrderJpaRepository jpa; @Test void findsPendingOrders() { // preparar entidades de prueba con la propia API var result = jpa.findByStatus(OrderStatus.PENDING); assertThat(result).hasSize(1); } } ''@DataJpaTest'' configura solo la infraestructura de persistencia, no el resto de la aplicación. La base de datos de pruebas debe ser la misma tecnología y versión que producción, en contenedor efímero con **Testcontainers** (mecanismo ya aplicado en 1.º DAW); una base de datos embebida de características distintas comprobaría otra cosa distinta. Las pruebas de persistencia comprueban el contrato con la base de datos. El resto de capas trabaja contra el repositorio abstraído y no necesita base de datos en sus pruebas unitarias: cada nivel se prueba con su realidad exacta.