JDBC es la API estándar de Java para conectar aplicaciones Java con bases de datos relacionales. Forma parte del JDK y permite ejecutar sentencias SQL, recuperar resultados y gestionar transacciones de forma independiente del motor de base de datos utilizado (MySQL, Oracle, PostgreSQL, etc.).
La portabilidad se consigue gracias a un sistema de drivers: cada fabricante proporciona su propio driver JDBC que implementa la interfaz estándar, de modo que el código de la aplicación no cambia aunque se cambie de base de datos.
| Componente | Paquete | Responsabilidad |
|---|---|---|
DriverManager |
java.sql |
Gestiona los drivers registrados y crea conexiones |
Connection |
java.sql |
Representa una sesión activa con la base de datos |
Statement |
java.sql |
Ejecuta SQL estático sin parámetros |
PreparedStatement |
java.sql |
Ejecuta SQL precompilado con parámetros |
CallableStatement |
java.sql |
Invoca procedimientos almacenados |
ResultSet |
java.sql |
Contiene las filas devueltas por una consulta |
El flujo estándar sigue siempre este orden:
DriverManager.getConnection(url, usuario, contraseña).ResultSet → Statement → Connection.Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection(URL, USER, PASS);
ps = conn.prepareStatement("SELECT nombre FROM empleados WHERE id = ?");
ps.setInt(1, 42);
rs = ps.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("nombre"));
}
} finally {
// Cierre en orden inverso
if (rs != null) rs.close();
if (ps != null) ps.close();
if (conn != null) conn.close();
}
Este es uno de los puntos más examinados. Cada objeto JDBC que se abre consume recursos tanto en la JVM como en el servidor de base de datos (cursores, memoria, sockets).
ResultSet → Statement / PreparedStatement → Connection
El motivo es la dependencia jerárquica: un ResultSet depende del Statement que lo creó, y un Statement depende de la Connection. Cerrar en orden inverso garantiza que no queden referencias huérfanas.
Importante: cerrar la
Connectiondirectamente también cierra losStatementyResultSetasociados, pero hacerlo explícitamente en orden es una buena práctica que evita fugas de recursos en entornos con pooling de conexiones.
La forma moderna y preferida usa AutoCloseable:
try (Connection conn = DriverManager.getConnection(URL, USER, PASS);
PreparedStatement ps = conn.prepareStatement("SELECT * FROM productos");
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("nombre"));
}
} // Los tres recursos se cierran automáticamente al salir del bloque
Ejecuta SQL construido como cadena de texto. Se usa para consultas que no varían ni reciben datos del usuario.
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT * FROM paises");
Inconveniente principal: si se concatena input del usuario, es vulnerable a inyección SQL.
Ejecuta una plantilla SQL con marcadores de posición (?). La consulta se precompila una sola vez en el servidor y después se ejecuta con distintos valores.
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM usuarios WHERE email = ? AND activo = ?"
);
ps.setString(1, email);
ps.setBoolean(2, true);
ResultSet rs = ps.executeQuery();
| Ventaja | Explicación |
|---|---|
| Seguridad frente a SQL Injection | Los parámetros se tratan como datos, nunca como código SQL. Un atacante no puede alterar la estructura de la consulta. |
| Rendimiento | La consulta se compila una vez; en ejecuciones repetidas solo se envían los parámetros. Se compila una vez, se puede ejecutar varias veces |
| Legibilidad y mantenimiento | El código SQL queda separado de los valores, haciendo el código más limpio. |
| Versatilidad | Sirve para SELECT, INSERT, UPDATE y DELETE. No está limitado a consultas de lectura. |
Statement.? es opcional.Especializado para invocar procedimientos almacenados. Puede manejar parámetros de entrada (IN), salida (OUT) y entrada/salida (INOUT).
CallableStatement cs = conn.prepareCall("{call calcular_bonificacion(?, ?)}");
cs.setInt(1, empleadoId);
cs.registerOutParameter(2, Types.DECIMAL);
cs.execute();
BigDecimal bonificacion = cs.getBigDecimal(2);
La inyección SQL ocurre cuando datos proporcionados por el usuario se insertan directamente en una consulta SQL sin sanitizar:
// PELIGROSO
String sql = "SELECT * FROM usuarios WHERE nombre = '" + inputUsuario + "'";
Si inputUsuario vale ' OR '1'='1, la consulta devuelve todos los registros.
Con PreparedStatement esto es imposible porque el driver JDBC separa el código SQL de los datos:
// SEGURO
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM usuarios WHERE nombre = ?"
);
ps.setString(1, inputUsuario); // Tratado siempre como dato, nunca como SQL
| Método | Se usa con | Devuelve |
|---|---|---|
executeQuery() |
SELECT | ResultSet |
executeUpdate() |
INSERT, UPDATE, DELETE, DDL | int (filas afectadas) |
execute() |
Cualquier sentencia | boolean (true si hay ResultSet) |
Por defecto, un ResultSet es de solo avance (TYPE_FORWARD_ONLY) y solo lectura (CONCUR_READ_ONLY). Se puede configurar al crear el Statement:
Statement st = conn.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE, // Permite avanzar y retroceder
ResultSet.CONCUR_READ_ONLY
);
next() — avanza a la siguiente fila (devuelve false al final).previous() — retrocede (solo en scrollable).first() / last() — va a la primera/última fila.absolute(n) — va a la fila n.int id = rs.getInt("id");
String nombre = rs.getString("nombre");
double salario = rs.getDouble("salario");
Date fecha = rs.getDate("fecha_alta");
Por defecto JDBC opera en modo auto-commit: cada sentencia se confirma automáticamente. Para agrupar varias operaciones en una transacción atómica:
conn.setAutoCommit(false);
try {
ps1.executeUpdate(); // Operación 1
ps2.executeUpdate(); // Operación 2
conn.commit(); // Confirmar ambas
} catch (SQLException e) {
conn.rollback(); // Deshacer si algo falla
}
DriverManager
│
▼
Connection ──────────────────────────────────────────┐
│ │
├── createStatement() → Statement │
├── prepareStatement(sql) → PreparedStatement │
└── prepareCall(sql) → CallableStatement │
│ │
▼ │
executeQuery() → ResultSet │
executeUpdate() → int │
│ │
▼ │
┌─────────────┐ │
│ rs.close() │ 1º cerrar ResultSet │
│ ps.close() │ 2º cerrar Statement │
│conn.close() │ 3º cerrar Connection ────────────┘
└─────────────┘
ResultSet → Statement → Connection. Cerrar la Connection sin cerrar antes el ResultSet cierra ambos, pero se considera mala práctica.PreparedStatement protege contra inyección SQL porque separa el código SQL de los parámetros; estos se tratan siempre como datos.PreparedStatement no está limitado a SELECT ni a consultas con parámetros; admite cualquier sentencia SQL y puede usarse sin marcadores ?.setAutoCommit(false).Class.forName().