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.