9.3.-JDBC: Prepared statement
9.3. JDBC PreparedStatement¶
Resumen
En este tema se explica por qué PreparedStatement debe ser la opción habitual cuando una consulta SQL recibe valores externos. Su uso mejora la seguridad, reduce errores al construir SQL y permite que el SGBDR reutilice mejor los planes de ejecución.
En el punto anterior ya vimos cómo conectarnos a una base de datos y ejecutar consultas. Ahora nos centramos en una decisión concreta pero muy importante: cómo preparar consultas que necesitan parámetros.
Directamente relacionado con el RA9 del módulo de Programación, este tema trabaja la gestión de información en bases de datos manteniendo integridad y consistencia. En concreto, refuerza la escritura de código para consultar, almacenar, modificar y borrar información de forma segura.
| Código | Descripción |
|---|---|
| RA9 | Gestiona información almacenada en bases de datos manteniendo la integridad y consistencia de los datos. |
| CE c | Se ha escrito código para almacenar información en bases de datos. |
| CE d | Se han creado programas para recuperar y mostrar información almacenada en bases de datos. |
| CE e | Se han efectuado borrados y modificaciones sobre la información almacenada. |
1. El problema de construir SQL con texto¶
Un error habitual al empezar con JDBC es construir consultas concatenando cadenas. El código puede parecer sencillo, pero mezcla la estructura de la consulta con los datos que llegan desde la aplicación.
val nombre = "pepe"
val sql = "SELECT * FROM persona WHERE nombre = '$nombre'"
val statement = connection.createStatement()
val resultSet = statement.executeQuery(sql)
Lectura del ejemplo:
nombrerepresenta un dato externo que podría venir de un formulario.sqlse construye pegando ese valor dentro de la consulta.createStatement()crea una sentencia SQL normal.executeQuery(sql)envía la consulta completa al SGBDR.
El problema es que la base de datos recibe una cadena final ya montada. Si el valor de nombre contiene comillas, operadores o fragmentos SQL, la consulta puede romperse o incluso cambiar de significado.
Riesgo de inyección SQL
Concatenar valores externos dentro del SQL puede permitir inyección SQL. Aunque el ejemplo parezca pequeño, en una aplicación real los datos pueden venir de una persona usuaria, de una API o de otro sistema.
Busca el ejemplo PreparedSelectParametro y estudialo/ejecutalo.
2. Qué aporta PreparedStatement¶
PreparedStatement separa la estructura de la consulta de los valores que se le pasan. La consulta se escribe con marcadores ?, y después se asigna cada valor con métodos tipados.
val sql = "SELECT * FROM persona WHERE nombre = ?"
connection.prepareStatement(sql).use { statement ->
statement.setString(1, "pepe")
statement.executeQuery().use { resultSet ->
while (resultSet.next()) {
println(resultSet.getString("nombre"))
}
}
}
Lectura del ejemplo:
?indica que la consulta tiene un parámetro.setString(1, "pepe")asigna el primer parámetro de la consulta.executeQuery()ejecuta una consulta de lectura.resultSet.next()avanza por las filas devueltas.getString("nombre")recupera el valor de la columnanombre.usecierra tanto la sentencia como elResultSet.
La diferencia fundamental es que el valor "pepe" no se pega al SQL. JDBC lo envía como parámetro, y el driver se encarga de tratarlo como dato, no como parte de la sintaxis SQL.
Busca el ejemplo PreparedSelectParametro y estudialo/ejecutalo.
3. Plan de ejecución y reutilización¶
Cuando un SGBDR recibe una consulta, analiza cómo ejecutarla. Ese análisis genera un plan de ejecución, es decir, una estrategia para obtener los datos: qué índices usar, en qué orden acceder a las tablas, qué filtros aplicar, etc.
Si se construyen consultas distintas para cada valor, el SGBDR puede tratarlas como sentencias diferentes:
Aunque las consultas tienen la misma estructura, el texto final no es idéntico. Con PreparedStatement, la estructura se mantiene estable:
Después cambian los valores, no la forma de la consulta:
Esto facilita que el SGBDR pueda reutilizar mejor el análisis de la consulta. El impacto real depende del gestor, del driver y de la configuración, pero la idea docente es clara: una consulta parametrizada expresa mejor la intención y evita reconstruir SQL manualmente.
4. Inserciones con PreparedStatement¶
PreparedStatement no solo sirve para SELECT. También se usa en INSERT, UPDATE y DELETE.
data class PersonaNueva(val nombre: String, val email: String)
fun insertarPersona(connection: Connection, persona: PersonaNueva): Int {
val sql = "INSERT INTO persona (nombre, email) VALUES (?, ?)"
connection.prepareStatement(sql).use { statement ->
statement.setString(1, persona.nombre)
statement.setString(2, persona.email)
return statement.executeUpdate()
}
}
Lectura del ejemplo:
PersonaNuevarepresenta los datos que se quieren guardar.- La consulta tiene dos parámetros:
nombreyemail. setString(1, persona.nombre)rellena el primer?.setString(2, persona.email)rellena el segundo?.executeUpdate()ejecuta elINSERT.- La función devuelve el número de filas insertadas.
Un uso correcto comprobaría el resultado:
val filas = insertarPersona(connection, PersonaNueva("Ana", "ana@example.com"))
if (filas == 1) {
println("Persona insertada correctamente.")
} else {
println("No se ha insertado ningún registro.")
}
Busca el ejemplo InsertBasico y estudialo/ejecutalo.
5. Actualizaciones y borrados parametrizados¶
En operaciones que modifican o eliminan datos, PreparedStatement ayuda a evitar errores graves al combinar parámetros con WHERE.
fun actualizarEmail(connection: Connection, id: Int, email: String): Int {
val sql = "UPDATE persona SET email = ? WHERE id = ?"
connection.prepareStatement(sql).use { statement ->
statement.setString(1, email)
statement.setInt(2, id)
return statement.executeUpdate()
}
}
Lectura del ejemplo:
- El primer parámetro es el nuevo correo.
- El segundo parámetro es el identificador de la fila que se modifica.
WHERE id = ?evita actualizar toda la tabla.- El valor devuelto indica cuántas filas se han actualizado.
Para borrar, el patrón es parecido:
fun eliminarPersona(connection: Connection, id: Int): Int {
val sql = "DELETE FROM persona WHERE id = ?"
connection.prepareStatement(sql).use { statement ->
statement.setInt(1, id)
return statement.executeUpdate()
}
}
Comprobar filas afectadas
En UPDATE y DELETE, que executeUpdate() devuelva 0 puede significar que no existe ningún registro con ese identificador. No siempre es una excepción técnica, pero sí es una situación que la aplicación debe gestionar.
Busca los ejemplos UpdateBasico y DeleteBasico y estudialos/ejecutalos.
6. Logs y consultas preparadas¶
Cuando se usa PreparedStatement, puede surgir una duda: si la consulta contiene ?, ¿cómo se registra en logs la consulta real?
Este log muestra la plantilla de la consulta, pero no el valor del parámetro. En ocasiones eso es suficiente, porque evita registrar datos personales o sensibles.
Algunos drivers permiten registrar la sentencia preparada con los parámetros ya aplicados:
val sql = "SELECT * FROM persona WHERE nombre = ?"
connection.prepareStatement(sql).use { statement ->
statement.setString(1, nombre)
logger.debug(statement.toString())
}
En determinados drivers, la salida puede incluir algo parecido a:
Cuidado con los logs
Registrar parámetros puede ayudar a depurar, pero también puede exponer información sensible. No conviene registrar contraseñas, tokens, datos personales innecesarios ni cadenas de conexión.
Busca el ejemplo GestionSQLException y estudialo/ejecutalo.
7. Buenas prácticas¶
- Usar
PreparedStatementsiempre que haya parámetros. - Evitar concatenar datos externos dentro del SQL.
- Usar métodos tipados como
setString,setIntosetDouble. - Comprobar el resultado de
executeUpdate(). - Cerrar
PreparedStatementyResultSetconuse. - Registrar errores sin exponer información sensible.
Busca el ejemplo CierreRecursosUse y estudialo/ejecutalo.
8. Conclusión¶
PreparedStatement es una herramienta básica para escribir acceso a datos seguro y mantenible. Permite separar SQL y datos, reduce errores al construir consultas, ayuda a prevenir inyección SQL y facilita que las operaciones parametrizadas se expresen de forma clara.
La idea clave es que una consulta con parámetros no debe construirse pegando texto. Debe prepararse una vez como estructura SQL y recibir los valores de forma controlada.
Fuentes y referencias¶
- Ejemplos U9
- Programación - 08 Programación con Bases de Datos - José Luis González
- The Java Tutorials - Using Prepared Statements