Tabla de Contenidos

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<Book> findById(long id);
    List<Book> 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<Book> 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<Book> 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.