====== 05 - La capa de persistencia ====== La **capa de persistencia** aísla el acceso a datos del resto de la aplicación. En la arquitectura por capas, el dominio define la **interfaz** de repositorio que necesita y esta capa la implementa con JDBC. ===== 1. DAO: interfaz e implementación ===== Un **DAO** (*Data Access Object*) agrupa las operaciones de acceso a una tabla o agregado. La interfaz vive junto al dominio; la implementación JDBC, en la capa de persistencia: // domain/repository/BookRepository.java public interface BookRepository { Optional findById(long id); List findByTitle(String title); void save(Book book); } // persistence/repository/impl/BookRepositoryImpl.java public class BookRepositoryImpl implements BookRepository { private final DbConnection connection; public BookRepositoryImpl(DbConnection connection) { this.connection = connection; } @Override public Optional findById(long id) { String sql = "SELECT id, title, price FROM book WHERE id = ?"; try (Connection conn = connection.get(); PreparedStatement statement = conn.prepareStatement(sql)) { statement.setLong(1, id); try (ResultSet resultSet = statement.executeQuery()) { return resultSet.next() ? Optional.of(map(resultSet)) : Optional.empty(); } } catch (SQLException exception) { throw new DataAccessException("Error finding book " + id, exception); } } private Book map(ResultSet resultSet) throws SQLException { return new Book( resultSet.getLong("id"), resultSet.getString("title"), resultSet.getBigDecimal("price")); } } ^ Decisión ^ Razón ^ | Interfaz definida por el dominio | Inversión de dependencias: el dominio no conoce JDBC | | Mapeo en un método privado | Traduce ''ResultSet'' a modelo en un único punto | | Excepciones propias | La aplicación no depende de ''SQLException'' | ===== 2. Qué le corresponde ===== ^ Le corresponde ^ No le corresponde ^ | Ejecutar SQL y mapear resultados | Reglas de negocio (precios, validaciones de dominio) | | Gestionar conexiones y transacciones | Decidir si una operación está permitida | | Traducir errores de BD a excepciones propias | Construir respuestas HTTP | ===== 3. Pruebas de persistencia ===== Las pruebas de la capa de persistencia necesitan una base de datos **real**: con H2 o mocks se prueba el código, no la base. **Testcontainers** levanta un contenedor **MariaDB con la misma versión** que producción para cada prueba: @Testcontainers class BookRepositoryImplTest { @Container static MariaDBContainer mariadb = new MariaDBContainer<>("mariadb:12.3"); BookRepositoryImpl repository; @BeforeAll static void startDatabase() { // aplicar migraciones y crear el repositorio contra el contenedor } @Test void savesAndFindsBook() { Book book = new Book("Clean Code", new BigDecimal("34.50")); repository.save(book); Optional found = repository.findById(book.id()); assertTrue(found.isPresent()); assertEquals("Clean Code", found.get().title()); } } ^ Qué se comprueba aquí ^ Qué no ^ | SQL correcto contra el motor real | Reglas de negocio (pruebas de dominio) | | Restricciones, claves, ''NOT NULL'' | Consultas HTTP (pruebas de controller) | | Mapeo ''ResultSet'' a modelo | Velocidad o rendimiento | ===== 4. Migraciones ===== El esquema se crea y evoluciona con **migraciones versionadas** que se aplican en orden, en desarrollo y en las pruebas: db/migration/V1__create_publisher_and_book.sql db/migration/V2__add_book_author_table.sql La capa de persistencia es la única que habla SQL. Si un service construye una consulta, el límite de capas se ha roto; las pruebas de arquitectura con ArchUnit lo detectan.