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.
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.
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");
}
}
}
? se sustituye con setLong, setString…try-with-resources cierra Connection, PreparedStatement y ResultSet aunque ocurra una excepción.| 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.
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());
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.
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()));
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.
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ó.