W3docs

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étodoUsar paraDevuelve
executeQuery(sql)SELECTun ResultSet
executeUpdate(sql)INSERT / UPDATE / DELETE / DDLint filas afectadas
execute(sql)desconocido / múltiples resultadosboolean (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.

Advertencia

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.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • Las constantes del cursor son simples ints que se pasan a createStatement. TYPE_FORWARD_ONLY + CONCUR_READ_ONLY es el valor predeterminado y el más económico; solo solicitas un cursor desplazable o actualizable cuando realmente lo necesitas.
  • Statement.RETURN_GENERATED_KEYS es el indicador que permite que un INSERT te devuelva el nuevo id de autoincremento a través de getGeneratedKeys() — sin él no puedes recuperar la clave asignada por la base de datos.
  • La primera consulta concatenada es inofensiva porque Acme no 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 SELECT previsto se convierte en un SELECT seguido de un DROP 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 PreparedStatement envíe la plantilla y los datos por separado — el tema del siguiente capítulo.

Práctica

Práctica
Tu código construye una consulta concatenando un valor de un formulario web directamente en la cadena SQL después de WHERE owner =. ¿Cuál es la corrección correcta?
Tu código construye una consulta concatenando un valor de un formulario web directamente en la cadena SQL después de WHERE owner =. ¿Cuál es la corrección correcta?
Was this page helpful?