Statement y PreparedStatement

Ejecución de SQL y prevención de SQL injection

Statement

Statement ejecuta SQL sin parámetros. Solo es apropiado para SQL completamente estático donde todos los valores están en el código fuente — nunca para SQL que incluya datos del usuario o externos.

import java.sql.*;

// Statement: ejecutar SQL estático (sin parámetros)
try (Connection con = dataSource.getConnection();
     Statement st = con.createStatement()) {

    // executeQuery: para SELECT — devuelve ResultSet
    ResultSet rs = st.executeQuery("SELECT id, nombre FROM clientes WHERE activo = TRUE");

    // executeUpdate: para INSERT/UPDATE/DELETE — devuelve filas afectadas
    int filas = st.executeUpdate("UPDATE clientes SET activo = FALSE WHERE id = 99");

    // execute: para cualquier SQL — devuelve true si hay ResultSet
    boolean tieneResultSet = st.execute("CALL mi_procedimiento()");
}

SQL Injection

La vulnerabilidad más crítica en aplicaciones con bases de datos. Concatenar parámetros directamente en el SQL permite a un atacante inyectar comandos SQL arbitrarios: extraer datos confidenciales, modificar registros, o destruir tablas.

// SQL Injection: el problema de concatenar parámetros en el SQL

// VULNERABLE: nunca hagas esto
String nombre = request.getParameter("nombre"); // podría ser: "'; DROP TABLE clientes;--"
String sql = "SELECT * FROM clientes WHERE nombre = '" + nombre + "'"; // ¡PELIGROSO!
// Si nombre = "' OR '1'='1", la consulta devuelve TODOS los clientes
// Si nombre = "'; DROP TABLE clientes;--", destruye la tabla

// SQL Injection es la vulnerabilidad #1 según OWASP
// La solución: PreparedStatement con parámetros

PreparedStatement

El SQL se precompila con ? como marcadores de posición. Los valores se pasan separados del SQL — el driver los escapa correctamente. El motor de BBDD recibe la estructura SQL fija y los datos como parámetros separados, haciendo imposible la inyección. Úsalo siempre que el SQL incluya datos externos.

// PreparedStatement: SQL precompilado con parámetros — previene SQL injection

String sql = "SELECT * FROM clientes WHERE nombre = ? AND activo = ?";

try (Connection con = dataSource.getConnection();
     PreparedStatement ps = con.prepareStatement(sql)) {

    // Los ? se sustituyen de forma segura: el driver escapa los valores
    ps.setString(1, nombre);   // primer ?
    ps.setBoolean(2, true);    // segundo ?

    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getLong("id") + ": " + rs.getString("nombre"));
        }
    }
}
// Cualquier intento de inyección en 'nombre' queda como literal, no como SQL

Batch updates

Para insertar o actualizar muchos registros, el batch reduce drásticamente el número de round-trips entre la aplicación y la base de datos. En lugar de N llamadas a executeUpdate(), se acumulan en el driver y se envían de una vez.

// Batch update: ejecutar múltiples sentencias de una vez — mucho más eficiente
// que ejecutarlas de una en una

String sqlInsert = "INSERT INTO log_eventos (tipo, mensaje, fecha) VALUES (?, ?, ?)";

try (Connection con = dataSource.getConnection();
     PreparedStatement ps = con.prepareStatement(sqlInsert)) {

    con.setAutoCommit(false); // una sola transacción para todo el batch

    for (Evento evento : eventos) {
        ps.setString(1, evento.getTipo());
        ps.setString(2, evento.getMensaje());
        ps.setTimestamp(3, Timestamp.valueOf(evento.getFecha()));
        ps.addBatch(); // acumula en el batch
    }

    int[] resultados = ps.executeBatch(); // envía todo de una vez
    con.commit();
    System.out.println("Insertados: " + resultados.length + " registros");
}

IDs autogenerados

// Obtener el ID autogenerado tras un INSERT
String sql = "INSERT INTO clientes (nombre, email) VALUES (?, ?)";

try (Connection con = dataSource.getConnection();
     PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {

    ps.setString(1, "Carlos Pérez");
    ps.setString(2, "carlos@ejemplo.com");
    ps.executeUpdate();

    try (ResultSet keys = ps.getGeneratedKeys()) {
        if (keys.next()) {
            long nuevoId = keys.getLong(1);
            System.out.println("Cliente creado con id: " + nuevoId);
        }
    }
}
Regla: usa PreparedStatement para todo SQL que incluya datos variables. Statement solo es válido para SQL completamente estático en el código fuente. La diferencia de rendimiento entre ambos es mínima — el ahorro de la precompilación compensa con creces.

Siguiente apartado → ResultSet: navegación y mapeo de filas a objetos

Índice de la sección

Índice del curso