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<OrderLineEntity> 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.
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<OrderEntity, Long> { List<OrderEntity> 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<OrderEntity> 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<OrderEntity> findById(long id); List<OrderEntity> findByStatus(OrderStatus status); } @Named class OrderDaoImpl implements OrderDao { private final OrderJpaRepository jpa; @Inject public OrderDaoImpl(OrderJpaRepository jpa) { this.jpa = jpa; } public Optional<OrderEntity> 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<Order> 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<Order> 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.
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); }
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.
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.