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.