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