Tabla de Contenidos

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:

<dependency>
  <groupId>org.mariadb.jdbc</groupId>
  <artifactId>mariadb-java-client</artifactId>
  <version>3.5.3</version>
</dependency>

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");
        }
    }
}

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<Book> findById(long id);
    List<Book> findByTitle(String title);
    Book save(Book book);
    void deleteById(long id);
}

public class JdbcBookDao implements BookDao {

    @Override
    public Optional<Book> 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.