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.

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.

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.

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.

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.

La capa persistence compone tres piezas con diferentes responsabilidades sobre los datos:

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);

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);
    }
}

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.

domainpersistenceOrderRepositoryOrderServiceImplOrderOrderJpaRepository(Spring Data)OrderDaoOrderDaoImplOrderRepositoryImplOrderEntityOrderPersistenceMapperBase de datosutilizaimplementautilizaimplementautilizagestiona

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

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.

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.

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.
  • clase/daw/dws/1eval/jpa.txt
  • Última modificación: 2026/10/21 10:25
  • por cesguiro