Fundamentos de JDBC desde Kotlin
Habla con cualquier base de datos SQL desde Kotlin con JDBC puro: de forma segura, idiomatica y sin filtrar una sola conexion.
JDBC (Java Database Connectivity) es la forma mas universal y de mas bajo nivel de hablar con una base de datos relacional en la JVM. Toda herramienta de mas alto nivel que veras mas adelante en este curso — Spring Data JDBC, JPA/Hibernate, jOOQ, Exposed — se apoya en ultima instancia sobre ella. Entender JDBC directamente las desmitifica todas: aprendes que es realmente una conexion, por que importa el binding de parametros y donde se pueden filtrar recursos. Aunque nunca escribas JDBC puro en produccion, este modelo mental es lo que te permite depurar las abstracciones cuando se portan mal.
El punto de entrada es un DataSource, no una sola conexion. Un DataSource es una fabrica que te entrega conexiones de un pool bajo demanda; en una app de Spring Boot se autoconfigura (HikariCP por defecto) y se inyecta por ti. Le pides una Connection con dataSource.connection, haces tu trabajo y devuelves la conexion al pool cerrandola. Una Connection es un recurso costoso y de vida corta que representa una sesion abierta con la base de datos: la tomas prestada, la usas y la devuelves lo mas rapido posible. Mantener una abierta durante una llamada de red lenta o el tiempo de reflexion del usuario es una forma clasica de agotar el pool.
Nunca construyas SQL concatenando entrada del usuario. Una sentencia como "SELECT * FROM users WHERE email = '" + email + "'" es un agujero de inyeccion SQL de manual: un valor de email manipulado puede reescribir por completo tu consulta. La solucion es un PreparedStatement con marcadores posicionales (?) y parametros vinculados. El driver envia la plantilla de la consulta y los valores por separado, asi que la base de datos trata tus entradas estrictamente como datos, nunca como SQL ejecutable. Como ventaja extra, las sentencias preparadas pueden analizarse una vez y reutilizarse, lo que es mas rapido para consultas repetidas.
Los parametros se vinculan por indice basado en 1 usando setters tipados: setString(1, email), setLong(2, id), setBoolean(3, active), y asi sucesivamente. El indice se refiere a la posicion de cada ? en el SQL, de izquierda a derecha. Hacer coincidir el setter con el tipo de columna evita que el driver haga coerciones sorprendentes, y setNull(index, sqlType) es la forma correcta de vincular un NULL de SQL en lugar de pasar una cadena vacia. Como el valor nunca pasa a formar parte del texto SQL, incluso una entrada como "'; DROP TABLE users; --" se almacena y compara como la cadena literal que es, nada mas.
Leer resultados significa iterar un ResultSet. Llamar a executeQuery() devuelve un cursor posicionado antes de la primera fila; cada llamada a next() lo avanza y devuelve false cuando se acaban las filas, por eso el bucle canonico es while (rs.next()) { ... }. Dentro del bucle extraes columnas con getString("email"), getLong("id"), getInt, getBoolean y companeros — direccionar columnas por nombre es mas legible y mas resistente a refactorizaciones que por indice. Ten en cuenta que getInt sobre un NULL de SQL devuelve 0; cuando la nulabilidad importa, lee el valor y luego comprueba rs.wasNull().
Los tres objetos — Connection, PreparedStatement, ResultSet — retienen recursos nativos que deben liberarse, incluso cuando se lanza una excepcion. En Java recurres a try-with-resources; Kotlin te da la mas elegante extension use {}. Cualquier Closeable o AutoCloseable puede llamar a use { resource -> ... }, que ejecuta tu bloque y garantiza que se invoque close() despues, propagando tu excepcion si ocurre alguna y cerrando limpiamente en cualquier caso. Anidar bloques use (conexion, luego sentencia, luego result set) cierra todo en el orden inverso correcto sin el ruido de los finally.
El toque idiomatico de Kotlin es mapear una fila cruda a un tipo de dominio real en lugar de pasar un ResultSet por todo tu codigo. Dale a tu data class un companion object con una fabrica fromRow(rs: ResultSet): User. Esto mantiene el conocimiento fragil de nombre-de-columna-a-propiedad en exactamente un lugar: si se renombra una columna, cambias una funcion, no veinte puntos de llamada. Tu funcion de consulta se vuelve entonces una tuberia limpia — toma prestada una conexion, prepara y vincula, itera, y recolecta User.fromRow(rs) en una lista — con los detalles de la base de datos pulcramente encapsulados.
Una palabra sobre el panorama general: el JDBC clasico es bloqueante. Cada llamada a executeQuery() detiene el hilo que la invoca hasta que la base de datos responde, asi que debes mantenerlo fuera del hilo principal o del event-loop. En un servicio basado en corrutinas envuelves el trabajo bloqueante de JDBC en withContext(Dispatchers.IO), que lo entrega a un pool de hilos dimensionado para I/O bloqueante. (Para acceso a base de datos verdaderamente no bloqueante recurririas a R2DBC, un tema posterior.) Por ahora, interioriza el ritmo de cuatro tiempos — el DataSource da una Connection, el PreparedStatement vincula parametros, el ResultSet entrega filas y use {} cierra todo — y el resto del ecosistema de persistencia se sentira como comodidad encima de fundamentos que ya dominas.
import javax.sql.DataSourceimport java.sql.ResultSetdata class User(val id: Long, val email: String, val active: Boolean) {companion object {// Single source of truth for column -> property mapping.fun fromRow(rs: ResultSet) = User(id = rs.getLong("id"),email = rs.getString("email"),active = rs.getBoolean("active"),)}}class UserRepository(private val dataSource: DataSource) {fun findActiveByEmail(email: String): User? {val sql = "SELECT id, email, active FROM users WHERE email = ? AND active = ?"// Each use {} closes its resource even if an exception is thrown.return dataSource.connection.use { conn ->conn.prepareStatement(sql).use { stmt ->stmt.setString(1, email) // bound, not concatenatedstmt.setBoolean(2, true)stmt.executeQuery().use { rs ->if (rs.next()) User.fromRow(rs) else null}}}}}
The full safe-query pattern: nested use {} blocks guarantee Connection, PreparedStatement and ResultSet are always closed. Parameters are bound by 1-based index, never concatenated, so this is immune to SQL injection. Requires javax.sql.DataSource (Spring/JDBC + a DB), so it does not run in-browser.
import javax.sql.DataSourcefun UserRepository.findRecent(dataSource: DataSource, limit: Int): List<User> {val sql = "SELECT id, email, active, login_count FROM users ORDER BY id DESC LIMIT ?"val results = ArrayList<User>()dataSource.connection.use { conn ->conn.prepareStatement(sql).use { stmt ->stmt.setInt(1, limit)stmt.executeQuery().use { rs ->while (rs.next()) { // false when rows are exhaustedresults += User.fromRow(rs)// Nullable column example:val loginCount = rs.getInt("login_count")val realCount: Int? = if (rs.wasNull()) null else loginCount// ... use realCount as needed}}}}return results}
Iterating many rows into a list, plus correct NULL handling with wasNull(). getInt returns 0 for a SQL NULL, so read-then-check is the only safe way to distinguish 'zero' from 'absent'. dataSource is passed in because it is private to the repository.
import kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.runBlockingimport kotlinx.coroutines.withContext// Stand-in for a blocking JDBC call (Thread.sleep simulates DB latency).fun blockingQuery(email: String): String {Thread.sleep(50)return "user:" + email}suspend fun fetchUser(email: String): String =// Keep blocking I/O off the main/event-loop thread.withContext(Dispatchers.IO) { blockingQuery(email) }fun main() = runBlocking {val result = fetchUser("ada@example.com")println("Loaded " + result + " on " + Thread.currentThread().name)}
JDBC is blocking, so in a coroutine-based service push it onto Dispatchers.IO to avoid stalling the caller's thread. This snippet mirrors the wrapping pattern; in real code the block performs the JDBC work shown above.
Arena IDE🧠Comprueba tu comprensión
0/1 · 0/1 answered1. Why is a PreparedStatement with bound parameters (WHERE email = ?) safer than concatenating the email directly into the SQL string?