====== 05 - JDBC: entidades y DAO ======
**JDBC** (*Java Database Connectivity*) es la API estándar de Java para conectarse a bases de datos y ejecutar SQL. Cada motor (MariaDB, PostgreSQL...) proporciona un **driver** que la implementa:
org.mariadb.jdbc
mariadb-java-client
3.5.3
Con la base de datos a un lado y la aplicación al otro, este tema recorre el acceso a datos completo: **conectar**, **ejecutar** sentencias, **mapear** filas a **entidades** y organizar las operaciones en un **DAO**.
===== 1. Conexión =====
La configuración se externaliza en un fichero de propiedades, no en el código:
db.url=jdbc:mariadb://localhost:3306/library
db.user=library_user
db.password=secret
public final class DbConnection {
private static final Properties PROPERTIES = loadProperties();
public static Connection get() throws SQLException {
return DriverManager.getConnection(
PROPERTIES.getProperty("db.url"),
PROPERTIES.getProperty("db.user"),
PROPERTIES.getProperty("db.password"));
}
}
''DriverManager.getConnection'' abre una conexión con el driver del classpath.
Una conexión abre y cierra por operación o por bloque lógico. Las conexiones son un recurso del servidor: mantenerlas abiertas innecesariamente agota la base de datos.
===== 2. PreparedStatement =====
String sql = "SELECT id, title, price, publisher_id FROM book WHERE publisher_id = ?";
try (Connection connection = DbConnection.get();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setLong(1, publisherId);
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
long id = resultSet.getLong("id");
String title = resultSet.getString("title");
BigDecimal price = resultSet.getBigDecimal("price");
}
}
}
* El marcador ''?'' se sustituye con ''setLong'', ''setString''...
* El **parametrizado** evita la **inyección SQL**: el valor nunca se interpreta como parte de la sentencia. Concatenar SQL con datos del usuario es un error grave.
* ''try-with-resources'' cierra ''Connection'', ''PreparedStatement'' y ''ResultSet'' aunque ocurra una excepción.
===== 3. Tipos de sentencias =====
^ Método ^ Uso ^
| ''executeQuery()'' | ''SELECT''; devuelve ''ResultSet'' |
| ''executeUpdate()'' | ''INSERT''/''UPDATE''/''DELETE''; devuelve filas afectadas |
| ''execute()'' | Sentencias generales; casi nunca hace falta |
Para recibir el ''id'' autogenerado por un ''INSERT'' se prepara la sentencia con ''Statement.RETURN_GENERATED_KEYS''; el ''save'' del DAO (apartado 5) lo muestra.
===== 4. Entidades =====
Una **entidad** es el objeto que representa una **fila** de una tabla: un campo por columna. Es con lo que el resto de la aplicación trabaja, sin saber que detrás hay SQL:
public record Book(long id, String title, BigDecimal price, long publisherId) {}
Los tipos de la tabla tienen equivalencia directa en Java:
^ Tipo SQL ^ Tipo Java ^ Lectura del ''ResultSet'' ^
| ''BIGINT''/''INT'' | ''long'', ''int'' | ''getLong''/''getInt'' |
| ''DECIMAL'' | ''BigDecimal'' | ''getBigDecimal'' |
| ''VARCHAR''/''TEXT'' | ''String'' | ''getString'' |
| ''BOOLEAN'' | ''boolean'' | ''getBoolean'' |
| ''DATE''/''TIMESTAMP'' | ''LocalDate''/''LocalDateTime'' | ''getObject'' |
La clave foránea entra como un **valor escalar**: la relación vive en la base de datos, en Java solo viaja ''publisherId''. La entidad no guarda referencias a otras entidades; quien necesite los datos de la editorial debe ir a la tabla ''publisher'' (otra consulta, o un ''JOIN'').
Del ''ResultSet'' a la entidad; la traducción se hace una vez, en un único método:
private Book map(ResultSet resultSet) throws SQLException {
return new Book(
resultSet.getLong("id"),
resultSet.getString("title"),
resultSet.getBigDecimal("price"),
resultSet.getLong("publisher_id"));
}
En el sentido inverso, los campos de la entidad alimentan los parámetros de la sentencia:
statement.setString(1, book.title());
statement.setBigDecimal(2, book.price());
statement.setLong(3, book.publisherId());
Para una columna que admite ''NULL'', el campo del record es ''Long'' (u otro objeto) y se lee con ''getObject("publisher_id")''. ''getLong'' devolvería ''0'' cuando la columna es ''NULL''.
===== 5. DAO =====
Un **DAO** (*Data Access Object*) agrupa en un único lugar las operaciones de acceso a una tabla. Se define con una **interfaz** — qué se ofrece, en términos de la aplicación — y una **implementación** — cómo se obtiene, con JDBC. La interfaz no menciona SQL ni JDBC:
public interface BookDao {
Optional findById(long id);
List findByTitle(String title);
Book save(Book book);
void deleteById(long id);
}
public class JdbcBookDao implements BookDao {
@Override
public Optional findById(long id) {
String sql = "SELECT id, title, price, publisher_id FROM book WHERE id = ?";
try (Connection connection = DbConnection.get();
PreparedStatement statement = connection.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);
}
}
@Override
public Book save(Book book) {
String sql = "INSERT INTO book (title, price, publisher_id) VALUES (?, ?, ?)";
try (Connection connection = DbConnection.get();
PreparedStatement statement = connection.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, book.title());
statement.setBigDecimal(2, book.price());
statement.setLong(3, book.publisherId());
statement.executeUpdate();
try (ResultSet keys = statement.getGeneratedKeys()) {
if (keys.next()) {
long id = keys.getLong(1);
return new Book(id, book.title(), book.price(), book.publisherId());
}
}
} catch (SQLException exception) {
throw new DataAccessException("Error saving book " + book.title(), exception);
}
throw new DataAccessException("INSERT did not return a generated key");
}
}
El método ''map'' es el del apartado 4; los métodos restantes (''findByTitle'', ''deleteById'') siguen el mismo esqueleto: abrir conexión, preparar sentencia, asignar parámetros, ejecutar y mapear.
^ Decisión ^ Razón ^
| Interfaz aparte de la implementación | El resto del código usa el DAO sin conocer JDBC; cambiar la implementación no le afecta |
| Mapeo en un método privado | La fila se traduce a entidad en un único punto |
| ''SQLException'' se traduce a ''DataAccessException'' | Las checked de JDBC no se propagan por toda la aplicación |
^ Le corresponde ^ No le corresponde ^
| Ejecutar SQL y mapear ''ResultSet'' a entidades | Reglas de negocio (precios, validaciones de dominio) |
| Gestionar conexiones, parámetros y transacciones | Decidir si una operación está permitida |
| Traducir errores de BD a excepciones propias | Construir la salida de la aplicación (consola, HTTP...) |
Uso mínimo:
BookDao bookDao = new JdbcBookDao();
Book cleanCode = bookDao.save(new Book(0, "Clean Code", new BigDecimal("34.50"), 1));
bookDao.findById(cleanCode.id()).ifPresent(b -> System.out.println(b.title()));
===== 6. Transacciones =====
Por defecto cada sentencia se confirma al ejecutarse (**autocommit**). Cuando varias operaciones deben ser **atómicas** — todas o ninguna — se desactiva y se gestiona explícitamente, siempre sobre la misma ''Connection'':
Connection connection = DbConnection.get();
try {
connection.setAutoCommit(false);
// insertar pedido, actualizar stock...
connection.commit();
} catch (SQLException exception) {
connection.rollback();
throw exception;
} finally {
connection.setAutoCommit(true);
connection.close();
}
Si algo falla a mitad de camino, ''rollback()'' deshace todo lo pendiente.
===== 7. SQLException =====
''SQLException'' informa del problema: ''getErrorCode()'' y ''getSQLState()'' permiten distinguir, por ejemplo, una violación de clave única de un fallo de conexión. El DAO la traduce a una excepción propia (''DataAccessException''), con el contexto de la operación que falló.
Para comprobar que un DAO funciona de verdad hace falta una base de datos real: las pruebas de integración con **Testcontainers** levantan un contenedor con la **misma versión de MariaDB** (se explican en el módulo de Entornos) sobre el esquema aplicado con las **migraciones** del tema 07.