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