Java JDBC Statement
Ejecuta SQL en Java con la interfaz Statement: cuándo usarla frente a PreparedStatement.
Un Statement envía una cadena SQL completa y fija a la base de datos. Se crea a partir de una Connection, se le pasa SQL y se obtiene de vuelta un ResultSet (para consultas) o un conteo de actualizaciones (para cambios). Es el más simple de los tres tipos de instrucción JDBC — y el que menos deberías usar, porque cualquier dato variable en el SQL debe concatenarse manualmente, lo que es el origen de los errores de inyección SQL.
Este capítulo explica cómo crear y ejecutar un Statement, los tres métodos de ejecución y cuándo aplica cada uno, cómo ajustar el cursor y leer claves generadas, y — lo más importante — cuándo detenerse y usar PreparedStatement en su lugar. Si eres nuevo en JDBC, empieza con la introducción a JDBC.
Creación y ejecución
try (Connection conn = DriverManager.getConnection(url, user, pw);
Statement st = conn.createStatement()) {
// a query → ResultSet
try (ResultSet rs = st.executeQuery("SELECT count(*) FROM product")) {
rs.next();
System.out.println(rs.getInt(1));
}
// a change → update count
int rows = st.executeUpdate("UPDATE product SET active = true WHERE price > 0");
System.out.println(rows + " rows updated");
}Los tres métodos de ejecución
| Método | Usar para | Devuelve |
|---|---|---|
executeQuery(sql) | SELECT | un ResultSet |
executeUpdate(sql) | INSERT / UPDATE / DELETE / DDL | int filas afectadas |
execute(sql) | desconocido / múltiples resultados | boolean (true si hay un ResultSet) |
Usa executeQuery y executeUpdate siempre que sepas de antemano qué tipo de instrucción estás ejecutando — devuelven el tipo correcto directamente. Recurre a execute solo en herramientas genéricas (una consola SQL, un ejecutor de migraciones) donde el SQL no se conoce hasta el tiempo de ejecución; después debes llamar a getResultSet() o getUpdateCount() para obtener el resultado.
executeUpdate devuelve 0 para DDL como CREATE TABLE, y para INSERT/UPDATE/DELETE devuelve el número de filas afectadas — útil para confirmar que una actualización realmente coincidió con una fila.
Ajuste del cursor y claves generadas
Al crear una instrucción puedes elegir cómo se comporta el cursor resultante con createStatement(resultSetType, resultSetConcurrency) — por ejemplo TYPE_FORWARD_ONLY, CONCUR_READ_ONLY (el valor predeterminado y más rápido). Solicita TYPE_SCROLL_INSENSITIVE solo cuando necesites desplazarte hacia atrás por el resultado, y CONCUR_UPDATABLE solo cuando pretendas editar filas a través del cursor; ambas opciones tienen un mayor costo.
Para inserciones, pasa Statement.RETURN_GENERATED_KEYS y luego lee la clave primaria asignada por la base de datos con getGeneratedKeys():
try (Statement st = conn.createStatement()) {
st.executeUpdate(
"INSERT INTO product(name, price) VALUES ('Widget', 9.99)",
Statement.RETURN_GENERATED_KEYS);
try (ResultSet keys = st.getGeneratedKeys()) {
if (keys.next()) {
long newId = keys.getLong(1);
System.out.println("inserted id = " + newId);
}
}
}Sin ese indicador la llamada tiene éxito pero getGeneratedKeys() devuelve un ResultSet vacío, por lo que no puedes recuperar el nuevo id.
Cuándo NO usar Statement
En el momento en que cualquier parte del SQL provenga de una variable — un nombre de usuario, un id, un término de búsqueda — detente y usa PreparedStatement en su lugar. Concatenar valores en una cadena de Statement es inseguro: un valor que contiene una comilla puede cambiar el significado del comando. PreparedStatement también almacena en caché su plan de análisis, por lo que una consulta que ejecutas en un bucle es más rápida como instrucción preparada. El siguiente capítulo está dedicado a esa alternativa segura; para procedimientos almacenados, consulta CallableStatement.
Reserva Statement para SQL fijo sin valores: configuración de esquemas (CREATE TABLE …), DDL puntual o un SELECT codificado directamente sin partes variables.
Nunca cierres un Statement mientras todavía necesitas su ResultSet — cerrar la instrucción cierra cualquier resultado que haya producido. Usa un bloque try-with-resources, como en los ejemplos anteriores, para que cada uno se cierre en el orden correcto.
Un ejemplo práctico: las constantes del cursor y la trampa de la inyección
Este programa imprime las constantes de ajuste de ResultSet/Statement que se pasan al crear una instrucción, y luego demuestra de forma concreta por qué el SQL construido con cadenas es peligroso — mostrando lo que un valor malicioso hace al texto del comando.
Lo que se puede extraer de la ejecución:
- Las constantes del cursor son simples
ints que se pasan acreateStatement.TYPE_FORWARD_ONLY+CONCUR_READ_ONLYes el valor predeterminado y el más económico; solo solicitas un cursor desplazable o actualizable cuando realmente lo necesitas. Statement.RETURN_GENERATED_KEYSes el indicador que permite que unINSERTte devuelva el nuevo id de autoincremento a través degetGeneratedKeys()— sin él no puedes recuperar la clave asignada por la base de datos.- La primera consulta concatenada es inofensiva porque
Acmeno tiene metacaracteres SQL. Eso es exactamente por qué la concatenación de cadenas parece funcionar en las pruebas — y luego falla en producción con entrada del mundo real. - El segundo valor contiene una comilla y un punto y coma, por lo que el único
SELECTprevisto se convierte en unSELECTseguido de unDROP TABLE. Los datos escaparon de sus comillas y se convirtieron en SQL ejecutable — la definición de manual de inyección. - La solución nunca es "escapar las comillas tú mismo." Es dejar de construir SQL a partir de valores y dejar que
PreparedStatementenvíe la plantilla y los datos por separado — el tema del siguiente capítulo.