📝 JDBC
← Volver

JDBC — Java Database Connectivity

¿Qué es JDBC?

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.


Arquitectura y componentes principales

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

Ciclo de vida de una conexión JDBC

El flujo estándar sigue siempre este orden:

  1. Cargar el driver (opcional desde JDBC 4.0, se carga automáticamente via Service Provider).
  2. Obtener una conexión mediante DriverManager.getConnection(url, usuario, contraseña).
  3. Crear un Statement a partir de la conexión.
  4. Ejecutar la consulta SQL.
  5. Procesar el ResultSet si la operación devuelve datos.
  6. Cerrar los recursos en orden inverso: ResultSetStatementConnection.
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();
}

Gestión de recursos: por qué y en qué orden cerrar

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

Orden correcto de cierre

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 Connection directamente también cierra los Statement y ResultSet asociados, pero hacerlo explícitamente en orden es una buena práctica que evita fugas de recursos en entornos con pooling de conexiones.

Try-with-resources (Java 7+)

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

¿Qué ocurre si no se cierran?


Statement vs PreparedStatement vs CallableStatement

Statement

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.

PreparedStatement

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();

Ventajas del PreparedStatement

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.

Mitos sobre PreparedStatement

CallableStatement

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

Inyección SQL: qué es y cómo la evita PreparedStatement

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étodos de ejecución

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)

ResultSet: navegación y tipos

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

Métodos de navegación

Obtener valores

int    id     = rs.getInt("id");
String nombre = rs.getString("nombre");
double salario = rs.getDouble("salario");
Date   fecha   = rs.getDate("fecha_alta");

Gestión de transacciones

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
}

Resumen visual del ciclo JDBC

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  ────────────┘
     └─────────────┘

Puntos clave para el examen