9.3.-Sentencias JDBC y ResultSet
9.3. Sentencias JDBC y ResultSet¶
Resumen
En este punto profundizamos en el código JDBC. Veremos cuándo usar Statement, por qué PreparedStatement debe ser la opción habitual con parámetros y cómo recorrer un ResultSet para transformar filas SQL en objetos Kotlin.
En el punto anterior abrimos una conexión. Ahora necesitamos usar esa conexión para enviar SQL a la base de datos y leer la respuesta.
| 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. Tabla de trabajo¶
Para mantener los ejemplos sencillos usaremos una tabla clientes:
CREATE TABLE clientes (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL
);
Datos de prueba:
INSERT INTO clientes (nombre, email) VALUES
('Ana López', 'ana@example.com'),
('Juan Pérez', 'juan@example.com');
Y el modelo Kotlin equivalente:
Lectura del ejemplo:
- La tabla vive en la base de datos.
Clientevive en memoria durante la ejecución del programa.- El código JDBC debe transformar filas en objetos y objetos en parámetros SQL.
2. Statement: SQL fijo y sin datos externos¶
Statement permite ejecutar una cadena SQL completa. Puede ser válido para consultas fijas que no reciben datos externos.
import java.sql.Connection
fun contarClientes(connection: Connection): Int {
val sql = "SELECT COUNT(*) AS total FROM clientes"
connection.createStatement().use { statement ->
statement.executeQuery(sql).use { resultSet ->
return if (resultSet.next()) {
resultSet.getInt("total")
} else {
0
}
}
}
}
Lectura del ejemplo:
- La consulta no recibe valores de la persona usuaria.
createStatement()crea una sentencia simple.executeQuery(sql)se usa porque la consulta devuelve filas.ResultSetcontiene el resultado de la consulta.resultSet.next()mueve el cursor a la primera fila.
Busca el ejemplo StatementSoloLectura y estúdialo o ejecútalo.
Límite de Statement
Si la consulta necesita valores variables, no conviene construir SQL concatenando texto. En ese caso debe usarse PreparedStatement.
3. El problema de concatenar SQL¶
Un error habitual al empezar con JDBC es construir consultas pegando cadenas.
val email = "ana@example.com"
val sql = "SELECT id, nombre, email FROM clientes WHERE email = '$email'"
val statement = connection.createStatement()
val resultSet = statement.executeQuery(sql)
El código parece sencillo, pero mezcla la estructura de la consulta con un dato externo.
Riesgos:
- La consulta puede romperse si el dato contiene comillas.
- El dato puede modificar el significado del SQL.
- Aumenta el riesgo de inyección SQL.
- El código queda menos claro y más difícil de probar.
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 un formulario, de una API o de otro sistema.
4. PreparedStatement: SQL con parámetros¶
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.
import java.sql.Connection
fun buscarClientePorEmail(connection: Connection, email: String): Cliente? {
val sql = """
SELECT id, nombre, email
FROM clientes
WHERE email = ?
""".trimIndent()
connection.prepareStatement(sql).use { statement ->
statement.setString(1, email)
statement.executeQuery().use { resultSet ->
return if (resultSet.next()) {
Cliente(
id = resultSet.getInt("id"),
nombre = resultSet.getString("nombre"),
email = resultSet.getString("email")
)
} else {
null
}
}
}
}
Lectura del ejemplo:
?indica que la consulta tiene un parámetro.setString(1, email)asigna el primer parámetro.- Los índices de parámetros empiezan en
1, no en0. executeQuery()devuelve unResultSet.- Si existe una fila, se transforma en
Cliente. - Si no hay resultados, se devuelve
null.
Busca el ejemplo PreparedSelectParametro y estúdialo o ejecútalo.
5. ResultSet: leer filas de una consulta¶
Un ResultSet representa el conjunto de filas devuelto por una consulta SELECT. Se recorre mediante un cursor.
fun obtenerClientes(connection: Connection): List<Cliente> {
val sql = "SELECT id, nombre, email FROM clientes ORDER BY nombre"
val clientes = mutableListOf<Cliente>()
connection.prepareStatement(sql).use { statement ->
statement.executeQuery().use { resultSet ->
while (resultSet.next()) {
clientes.add(
Cliente(
id = resultSet.getInt("id"),
nombre = resultSet.getString("nombre"),
email = resultSet.getString("email")
)
)
}
}
}
return clientes
}
Lectura del ejemplo:
executeQuery()se usa cuando esperamos filas.while (resultSet.next())recorre todas las filas.getIntygetStringleen columnas tipadas.- Cada fila se convierte en un objeto
Cliente. - La función devuelve una lista ya preparada para la aplicación.
Busca los ejemplos SelectBasico y MapeoFilaAObjeto y estúdialos o ejecútalos.
6. Operaciones que no devuelven filas¶
INSERT, UPDATE y DELETE no devuelven un ResultSet. Se ejecutan con executeUpdate(), que devuelve cuántas filas se han visto afectadas.
fun cambiarEmail(connection: Connection, id: Int, nuevoEmail: String): Int {
val sql = "UPDATE clientes SET email = ? WHERE id = ?"
connection.prepareStatement(sql).use { statement ->
statement.setString(1, nuevoEmail)
statement.setInt(2, id)
return statement.executeUpdate()
}
}
Lectura del ejemplo:
UPDATE clientes SET email = ?indica qué campo se modifica.WHERE id = ?limita la modificación a una fila concreta.executeUpdate()devuelve el número de filas actualizadas.- Si devuelve
0, puede significar que no existe ningún cliente con eseid.
Resultado funcional
Que una sentencia no lance excepción no significa que haya hecho lo esperado. En operaciones de escritura, comprobar las filas afectadas forma parte del diseño correcto.
7. 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.
Si se construyen consultas distintas para cada valor, el SGBDR puede tratarlas como sentencias diferentes:
SELECT * FROM clientes WHERE email = 'ana@example.com';
SELECT * FROM clientes WHERE email = 'juan@example.com';
Con PreparedStatement, la estructura se mantiene estable:
Después cambian los valores, no la forma 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.
8. Buenas prácticas¶
- Usar
PreparedStatementsiempre que haya parámetros. - Reservar
Statementpara SQL fijo y controlado por el programa. - Evitar concatenar datos externos dentro del SQL.
- Usar métodos tipados como
setString,setIntosetDouble. - Comprobar el resultado de
executeUpdate(). - Cerrar
Connection,PreparedStatementyResultSetconuse. - Registrar errores sin exponer información sensible.
Busca los ejemplos CierreRecursosUse y GestionSQLException y estúdialos o ejecútalos.
9. Cierre del punto¶
Este punto establece el bloque técnico central de JDBC:
Statementejecuta SQL fijo.PreparedStatementejecuta SQL parametrizado.ResultSetpermite recorrer filas.executeQuery()se usa para consultas de lectura.executeUpdate()se usa para operaciones que modifican datos.
En el siguiente punto usaremos estas piezas para construir aplicaciones CRUD organizadas mediante una capa de acceso a datos.
Fuentes y referencias¶
- Ejemplos U9
- Programación - 08 Programación con Bases de Datos - José Luis González
- The Java Tutorials - Using Prepared Statements