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'),
('Marta Ruiz', 'marta@example.com');
Una consulta de comprobación:
Salida esperada:
id | nombre | email
----+-------------+-------------------
1 | Ana López | ana@example.com
2 | Juan Pérez | juan@example.com
3 | Marta Ruiz | marta@example.com
Para clase
Antes de escribir código Kotlin, conviene comprobar la tabla desde el cliente SQL del gestor. Así separamos dos problemas: si la tabla no funciona en SQL, el error no está en Kotlin.
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. Consultas con filtros y agregaciones¶
Las consultas no siempre se limitan a recuperar todos los registros de una tabla. SQL permite filtrar, ordenar, agrupar y calcular resultados. Desde JDBC, la idea es la misma: preparar la consulta, asignar parámetros, ejecutar y leer el ResultSet.
Por ejemplo, si tenemos una tabla ventas con las columnas id, fecha, monto, tipo y sucursal, podríamos calcular el total de ventas con tarjeta en una sucursal concreta:
import java.sql.Connection
fun totalVentasTarjeta(connection: Connection, sucursal: String): Double {
val sql = """
SELECT SUM(monto) AS total
FROM ventas
WHERE sucursal = ?
AND tipo = ?
""".trimIndent()
connection.prepareStatement(sql).use { statement ->
statement.setString(1, sucursal)
statement.setString(2, "tarjeta")
statement.executeQuery().use { resultSet ->
return if (resultSet.next()) {
resultSet.getDouble("total")
} else {
0.0
}
}
}
}
Lectura del ejemplo:
- La consulta calcula un único dato: el total de ventas.
SUM(monto) AS totalasigna un nombre al resultado agregado.sucursal = ?ytipo = ?son filtros parametrizados.setStringasigna valores sin concatenar texto dentro del SQL.resultSet.next()comprueba si hay una fila con el resultado.getDouble("total")recupera el total calculado por la base de datos.
Este tipo de consulta es útil cuando no interesa traer todas las ventas a Kotlin para sumarlas una a una. Es más eficiente pedirle al SGBDR que filtre y agregue, porque está diseñado precisamente para ese tipo de operaciones.
7. 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.
8. 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.
9. 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 clientes WHERE email = ?"
connection.prepareStatement(sql).use { statement ->
statement.setString(1, email)
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 estúdialo o ejecútalo.
10. 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.
11. 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