⚙️ Conectores o drivers¶
Un conector o driver es un mecanismo que permite a un lenguaje de programación conectarse, y trabajar, contra una base de datos. Se encarga de mantener el diálogo con la base de datos, para poder llevar a cabo el acceso y manipulación de los datos.
Algunos de los más conocidos son:
-
ODBC (Open Database Connectivity). Es un estándar viejo. Esta tecnología proporciona una interfaz común para tener acceso a bases de datos SQL heterogéneas. ODBC está basado en SQL (Structured Query Language) como un estándar para tener acceso a datos. ODBC permite la conexión fácil desde varios lenguajes de programación y se utiliza mucho en el entorno Windows.
-
JDBC (Java Data Base Connectivity).
En este curso, nos vamos a centrar en JDBC, puesto que, desde el punto de vista de Java, es una de las tecnologías más importantes de conectividad a la base de datos. Y, además, Java 8 ha eliminado el puente JDBC-ODBC, lo que significa que los controladores ODBC de Microsoft ya no funcionan.
JDBC¶
Casi de forma simultánea a ODBC, la empresa Sun Microsystems, en 1997 sacó a la luz JDBC, un API conector de bases de datos, implementado específicamente para usar con el lenguaje Java. Se trata de un API bastante similar a ODBC en cuanto a funcionalidad, pero adaptado a las especificidades de Java. Es decir, la funcionalidad se encuentra capsulada en clases (ya que Java es un lenguaje totalmente orientado a objetos) y, además, no depende de ninguna plataforma específica, de acuerdo con la característica multiplataforma defendida por Java.
Es una API que permite la ejecución de operaciones contra una base de datos desde Java independientemente del sistema operativo donde se ejecute o de la base de datos a la cual se acceda.
Como vemos en la imagen, tenemos en la API una capa(JDBC Driver Manager) que aísla al programa del Gestor de Base de Datos. Lo único que será necesario es tener instalada la librería/driver del Sistema de Base de datos correspondiente que se encarga de los aspectos particulares de cada gestor(MySql, Oracle...)

Es importante destacar también que JDBC no exige ninguna instalación, ni ningún cambio sustancial en el código a la hora de utilizar uno u otro controlador. Esta característica se sustenta, en primer lugar, en la utilidad de Java que permite cargar programáticamente cualquier clase a partir de su nombre; en segundo lugar, en la funcionalidad de la clase DriverManager (de la API JDBC), que sin necesidad de indicarle el driver específico que hay que utilizar es capaz de encontrarlo y seleccionarlo de entre todos los que el sistema tenga cargados en memoria.
A pesar de eso tampoco es mucho problema ya que actualmente podemos encontrar un driver JDBC para prácticamente cualquier SGBDR existente. El conector lo proporciona el fabricante de la base de datos o bien un tercero.
Conexión con la BBDD desde JDBC¶
Antes de empezar a desarrollar aplicaciones JDBC es necesario aseguramos que tenemos instalado el SGBD, y además que tenemos acceso desde el lugar donde estemos desarrollando la aplicación. Una vez verificado el sistema gestor de base de datos, será necesario obtener el controlador JDBC del sistema gestor. Generalmente, cada fabricante pondrá a disposición de sus usuarios los diferentes tipos de controladores que tenga para sus productos. Sea cual sea el tipo de controlador que finalmente necesita, éste tendrá como mínimo una biblioteca en formato .jar con todas las clases de la API JDBC. Habrá que añadir el archivo .jar como biblioteca de nuestra aplicación.
Para descargar el driver JDBC para MySQL podemos hacerlo desde el repositorio de Maven:
Podemos añadirlo fácilmente en IntelliJ a nuestro proyecto

Establecimiento y cierre de conexión¶
Las clases que afectan a la gestión de la conexión con la BBDD son:
-
DriverManager: esta clase se utiliza para registrar el controlador para un tipo de base de datos específico (por ejemplo, MySQL en este tutorial) y para establecer una conexión de base de datos con el servidor a través de su métodogetConnection(). -
Connection, es una interfaz que representa una conexión a la base de datos establecida (sesión) desde la cual podemos crear declaraciones para ejecutar consultas y recuperar resultados, obtener metadatos sobre la base de datos, cerrar conexión, etc.Los objetosConnectionmantendrán la capacidad de comunicarse con el sistema gestor mientras permanezcan abiertos. Esto es, desde que se crean hasta que se cierran utilizando el método close.
El objeto Connection está totalmente vinculado a una fuente de datos, por eso en pedir la conexión hay que especificar de qué fuente se trata siguiendo el protocolo JDBC e indicando la url de los datos, y en su caso el usuario y password(si es necesario para el gestor de base de datos).
Ejemplos de uri de acceso

La url seguirá el protocolo JDBC, comenzará siempre por la palabra jdbc seguida de dos puntos. El resto dependerá del tipo de controlador utilizado, del host donde se aloje el SGBD, del puerto que este use para escuchar las peticiones y del nombre de la base de datos o esquema con el que queremos trabajar.
Como ocurre en la E/S, es necesario tratar la excepciones mediante try/catch
public static void main(String[] args) {
// ────────────────────────────────────────────────
// CONFIGURACIÓN DE LA CONEXIÓN
// ────────────────────────────────────────────────
// Usuario de la base de datos (recomendación: NO usar root en producción)
String user = "pepito";
// Contraseña del usuario (¡nunca dejar credenciales en el código en producción!)
String password = "grillo";
// URL de conexión JDBC para MySQL
// Formato general: jdbc:mysql://host:puerto/nombre_base_datos?parametros
String url = "jdbc:mysql://localhost/severo_ad";
// El try-with-resources asegura que la conexión se cierre automáticamente
// incluso si ocurre una excepción
try (final Connection connection = DriverManager.getConnection(url, user, password)) {
// Si llegamos aquí → la conexión fue exitosa
// Imprime el nombre de la base de datos actual (catálogo)
System.out.println("Base de datos conectada: " + connection.getCatalog());
// Ejemplos de otras consultas útiles que podrías hacer:
// System.out.println("AutoCommit: " + connection.getAutoCommit());
// System.out.println("Motor de base de datos: " + connection.getMetaData().getDatabaseProductName());
// System.out.println("Versión: " + connection.getMetaData().getDatabaseProductVersion());
} catch (SQLException ex) {
// ────────────────────────────────────────────────
// MANEJO DE ERRORES DETALLADO
// ────────────────────────────────────────────────
// Mensaje principal del error
System.err.println("SQLException: " + ex.getMessage());
// Código de estado SQL (estándar ANSI SQL)
System.err.println("SQLState: " + ex.getSQLState());
// Código de error específico del sistema (MySQL en este caso)
System.err.println("VendorError: " + ex.getErrorCode());
// Imprimir la pila completa (muy útil para depuración)
ex.printStackTrace();
// Algunos códigos de error comunes de MySQL:
// 1045 → usuario/contraseña incorrectos
// 1049 → base de datos no existe
// 0 → problema de conexión (servidor apagado, puerto incorrecto, etc.)
// 2003 → no se puede conectar al servidor (host o puerto)
}
System.out.println("Programa finalizado.");
}
}
CRUD(Crear, Leer, Actualizar, Eliminar)¶
CRUD es un acrónimo que se refiere a las operaciones básicas que se pueden realizar en una base de datos o sistema de gestión de bases de datos relacionales. Cada letra en CRUD representa una de estas operaciones

- Create (Crear): La operación de crear implica agregar nuevos registros o filas a una tabla en la base de datos.
- Read (Leer): La operación de leer implica recuperar información de la base de datos. Esto generalmente implica realizar consultas SELECT para recuperar datos de una o más tablas en la base de datos.
- Update (Actualizar): La operación de actualizar implica modificar los datos existentes en la base de datos. Esto se hace mediante la ejecución de consultas UPDATE que modifican los valores de uno o más registros en una tabla.
- Delete (Eliminar): La operación de eliminar implica eliminar registros o filas de una tabla en la base de datos. Esto se hace mediante la ejecución de consultas DELETE.
El CRUD en un programa define las operaciones básicas sobre la base de datos
El API JDBC distingue dos tipos de consultas:
- Consultas: SELECT
- Actualizaciones: INSERT, UPDATE, DELETE, sentencias DDL.
Interfaces y clases principales de JDBC¶
StatementyPreparedStatement: estas interfaces se utilizan para ejecutar consultas SQL estáticas y consultas SQL parametrizadas, respectivamente.Statementes la superinterfaz de la interfazPreparedStatement, que se utiliza para consultas parametrizadas.
Consultas sin parametrizar: Statement¶
Statement nos permite lanzar sentencias SQL
Sus métodos comúnmente utilizados son:
| Método | Devuelve | Usar para | Ejemplo típico |
|---|---|---|---|
executeQuery() |
ResultSet |
SELECT |
SELECT * FROM productos |
executeUpdate() |
int (filas afectadas) |
INSERT / UPDATE / DELETE / DDL |
UPDATE productos SET precio = 20 WHERE id = 1 |
execute() |
boolean |
Cuando no sabes si devuelve ResultSet o no |
SQL dinámico, procedimientos mixtos |
boolean execute(String sql): ejecuta una sentencia SQL general. Devuelve verdadero si la consulta devuelve unResultSet, falso si la consulta devuelve un recuento de actualizaciones o no devuelve nada. Este método solo se puede utilizar con una sentencia.
import java.sql.*; // Importamos todo lo necesario para JDBC
/**
* Ejemplo práctico del método Statement.execute(String sql)
* Muestra cómo distinguir entre consultas que devuelven ResultSet (SELECT)
* y las que no (INSERT, UPDATE, DELETE, CREATE, etc.)
*/
public class EjemploExecute {
public static void main(String[] args) {
// Datos de conexión (en producción usarían un archivo de configuración o variables de entorno)
String url = "jdbc:mysql://localhost:3306/tienda";
String usuario = "root";
String password = "tu_contraseña";
// try-with-resources → cierra automáticamente Connection y Statement
try (Connection conexion = DriverManager.getConnection(url, usuario, password);
Statement sentencia = conexion.createStatement()) {
// ────────────────────────────────────────────────
// Caso 1: Consulta SELECT → devuelve ResultSet
// ────────────────────────────────────────────────
boolean esConsultaSelect = sentencia.execute(
"SELECT id, nombre, precio FROM productos WHERE precio > 100"
);
if (esConsultaSelect) {
// getResultSet() solo se puede llamar cuando execute() devolvió true
try (ResultSet resultado = sentencia.getResultSet()) {
System.out.println("Resultados de la consulta SELECT:");
while (resultado.next()) {
System.out.printf("%3d | %-25s | $%8.2f%n",
resultado.getInt("id"),
resultado.getString("nombre"),
resultado.getDouble("precio"));
}
}
} else {
System.out.println("No era una consulta que devuelva ResultSet");
}
// ────────────────────────────────────────────────
// Caso 2: Sentencia de modificación (UPDATE)
// ────────────────────────────────────────────────
boolean esSelect2 = sentencia.execute(
"UPDATE productos SET precio = precio * 1.10 WHERE categoria = 'Electrónica'"
);
if (!esSelect2) {
// getUpdateCount() nos dice cuántas filas se modificaron
int filasAfectadas = sentencia.getUpdateCount();
System.out.println("UPDATE ejecutado → Filas afectadas: " + filasAfectadas);
}
// ────────────────────────────────────────────────
// Caso 3: Sentencia DDL (CREATE TABLE IF NOT EXISTS)
// ────────────────────────────────────────────────
boolean esSelect3 = sentencia.execute(
"""
CREATE TABLE IF NOT EXISTS auditoria (
id INT AUTO_INCREMENT PRIMARY KEY,
fecha DATETIME NOT NULL,
usuario VARCHAR(50) NOT NULL,
accion VARCHAR(100) NOT NULL,
detalle TEXT
)
"""
);
if (!esSelect3) {
// En DDL casi siempre getUpdateCount() = 0
System.out.println("CREATE TABLE ejecutado. Filas afectadas: " + sentencia.getUpdateCount());
}
// ────────────────────────────────────────────────
// Caso 4: INSERT que devuelve clave generada (auto-increment)
// ────────────────────────────────────────────────
boolean esSelect4 = sentencia.execute(
"""
INSERT INTO productos (nombre, precio, categoria)
VALUES ('Monitor 27" 144Hz', 289.99, 'Electrónica')
""",
Statement.RETURN_GENERATED_KEYS // ← importante para poder recuperar el ID
);
if (!esSelect4) {
int filasInsertadas = sentencia.getUpdateCount();
System.out.println("INSERT exitoso. Filas insertadas: " + filasInsertadas);
// Recuperamos las claves generadas (normalmente el ID auto-increment)
try (ResultSet claves = sentencia.getGeneratedKeys()) {
if (claves.next()) {
int nuevoId = claves.getInt(1);
System.out.println(" → ID del nuevo producto: " + nuevoId);
}
}
}
System.out.println("\nEjecución finalizada correctamente.");
} catch (SQLException e) {
System.err.println("Error al ejecutar la sentencia SQL:");
System.err.println("Mensaje: " + e.getMessage());
System.err.println("Código SQL: " + e.getSQLState());
System.err.println("Error número: " + e.getErrorCode());
e.printStackTrace();
}
}
}
int executeUpdate(String sql): ejecuta una sentencia INSERT, UPDATE o DELETE y devuelve un conteo actualizado que indica el número de filas afectadas (por ejemplo, 1 fila insertada, 2 filas actualizadas o 0 filas afectadas).
import java.sql.*;
public class EjemploExecuteUpdateBasico {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/tienda";
String user = "root";
String pass = "tu_contraseña";
try (Connection conn = DriverManager.getConnection(url, user, pass);
Statement stmt = conn.createStatement()) {
// 1. INSERT → normalmente devuelve 1
int filas1 = stmt.executeUpdate(
"""
INSERT INTO productos
(nombre, precio, categoria, stock)
VALUES ('Mouse gamer RGB', 34.90, 'Periféricos', 120)
"""
);
System.out.println("INSERT nuevo producto → filas afectadas: " + filas1);
// Salida típica: → filas afectadas: 1
// 2. UPDATE que afecta varias filas
int filas2 = stmt.executeUpdate(
"""
UPDATE productos
SET precio = precio * 1.12
WHERE categoria = 'Periféricos'
AND precio < 50.00
"""
);
System.out.println("Aumento de precio periféricos baratos → filas: " + filas2);
// Ejemplo salida: → filas: 8
// 3. DELETE que puede afectar 0 o más filas
int filas3 = stmt.executeUpdate(
"DELETE FROM productos WHERE stock <= 0"
);
System.out.println("Limpieza de productos sin stock → filas eliminadas: " + filas3);
// Posibles salidas: 0, 3, 15, etc.
// 4. INSERT recuperando ID autogenerado
int filas4 = stmt.executeUpdate(
"""
INSERT INTO clientes
(nombre, email, telefono, fecha_alta)
VALUES ('Valeria Torres', 'valeria.t85@gmail.com', '612345789', CURDATE())
""",
Statement.RETURN_GENERATED_KEYS
);
System.out.println("Cliente insertado → filas: " + filas4);
try (ResultSet rs = stmt.getGeneratedKeys()) {
if (rs.next()) {
System.out.println("→ ID generado: " + rs.getLong(1));
}
}
// 5. DDL (CREATE) → casi siempre devuelve 0
int resultadoDDL = stmt.executeUpdate(
"""
CREATE TABLE IF NOT EXISTS historial_precios (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
producto_id INT NOT NULL,
precio_anterior DECIMAL(10,2),
precio_nuevo DECIMAL(10,2),
fecha_cambio DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""
);
System.out.println("Creación tabla historial → resultado: " + resultadoDDL);
// Normalmente: resultado: 0
} catch (SQLException e) {
System.err.println("Error SQL:");
System.err.println("→ " + e.getMessage());
System.err.println("SQLState: " + e.getSQLState());
System.err.println("VendorCode: " + e.getErrorCode());
}
}
}
-
executeQuery(String sql): ejecuta una sentencia SELECT y devuelve un objetoResultSetque contiene los resultados devueltos por la consulta.Statement stmt = con.createStatement(); //compone la sentencia SQL String q1 = "SELECT * FROM USER WHERE id = '" + id + "' AND pwd = '" + pwd + "'"; //recibimos el resultado ResultSet rs = stmt.executeQuery(q1);
ResultSet¶
ResultSet: contiene los datos de la tabla devueltos por una consulta SELECT. Este objeto se usa para iterar sobre las filas en el conjunto de resultados usando el método next(). En todo momento, el ResultSet apunta a una fila en concreto a la podemos extraer los datos. Mediante el método next() pasamos a la siguiente fila

import java.sql.*;
public class EjemploResultSet {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/tienda";
String usuario = "root";
String clave = "tu_contraseña";
// ------------------------------------------------------------------------
// Ejemplo 1 – Forma clásica y más común
// ------------------------------------------------------------------------
try (Connection conn = DriverManager.getConnection(url, usuario, clave);
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(
"""
SELECT id, nombre, precio, categoria, stock, fecha_alta
FROM productos
WHERE precio <= 150.00
ORDER BY precio DESC
LIMIT 15
"""
)) {
// ResultSet empieza ANTES de la primera fila
// next() mueve el cursor a la siguiente fila y devuelve true si hay datos
int contador = 0;
while (rs.next()) { // Si no hay más filas devuelve false
contador++;
//obtenemos los datos de las columna de la tupla actual
int id = rs.getInt("id"); // por nombre de columna
String nombre = rs.getString("nombre");
double precio = rs.getDouble("precio");
String categoria = rs.getString("categoria");
int stock = rs.getInt(5); // también se puede por posición
Date fechaAlta = rs.getDate(6);
System.out.printf("%4d | %-28s | %8.2f | %-18s | %5d | %s%n",
id, nombre, precio, categoria, stock,
fechaAlta != null ? fechaAlta.toString() : "—");
}
System.out.println("──────────────────────────────────────────────────────");
System.out.println("Total productos encontrados: " + contador);
} catch (SQLException e) {
System.err.println("Error al consultar productos:");
e.printStackTrace();
}
//si existencias acepta nulos
int stock = rs.getInt("existencias");
// Si en la DB es NULL, JDBC devuelve 0 y stock será 0.
// ¡Error! Estás diciendo que no hay producto, cuando en realidad no sabemos cuántos hay. Si queremos mantener el los nulos
Tenemos el método wasNul() que nos indica si la última lectura de una columna es un NULL SQL. El procedimento suele ser: Primero lees el dato, luego preguntas si fue nulo.
Integer stock = rs.getInt("existencias"); // 1. Intentas leer
if (rs.wasNull()) { // 2. Preguntas: "¿Era nulo?"
stock = null; // 3. Si sí, asignas null (usando el Wrapper Integer)
System.out.println("Dato desconocido");
} else {
System.out.println("El stock real es: " + stock);
}
El siguiente ejemplo controla esta posible columna que puede aceptar nulos.
// ------------------------------------------------------------------------
// Ejemplo 2 – Uso con nombres de columnas vs índices + manejo de nulos
// ------------------------------------------------------------------------
try (Connection conn = DriverManager.getConnection(url, usuario, clave);
PreparedStatement ps = conn.prepareStatement(
"SELECT codigo, nombre, precio_venta, existencias, imagen_url " +
"FROM articulos WHERE categoria = ? AND existencias > 0 " +
"ORDER BY nombre LIMIT 8"
)) {
ps.setString(1, "Accesorios");
try (ResultSet rs = ps.executeQuery()) {
System.out.println("\nAccesorios disponibles:");
while (rs.next()) {
String codigo = rs.getString("codigo");
String nombre = rs.getString("nombre");
double precio = rs.getDouble("precio_venta");
// Manejo cuidadoso de posibles nulos.
//si es nulo en la base de datos, devuelve 0
Integer stock = rs.getInt("existencias");
//mantenemos el nulo
if (rs.wasNull()) stock = null;
//controlamos el nulo de una imagen con una imagen por defecto
String imagen = rs.getString("imagen_url");
if (rs.wasNull()) imagen = "sin_imagen.png";
System.out.printf(" • [%s] %s - €%.2f (stock: %s) %s%n",
codigo, nombre, precio, stock, imagen);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
// ------------------------------------------------------------------------
// Ejemplo 3 – Solo contar filas
// ------------------------------------------------------------------------
try (Connection conn = DriverManager.getConnection(url, usuario, clave);
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM productos WHERE stock = 0")) {
if (rs.next()) {
int sinStock = rs.getInt(1);
System.out.println("\nProductos sin stock: " + sinStock);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
Métodos de ResultSet¶
Los métodos principales de ResultSet son
| Método | Descripción | Ejemplo |
|---|---|---|
next() |
Avanza al siguiente registro del resultado | while(rs.next()) |
previous() |
Va al registro anterior | rs.previous() |
first() |
Va al primer registro | rs.first() |
last() |
Va al último registro | rs.last() |
beforeFirst() |
Posiciona antes del primer registro | rs.beforeFirst() |
afterLast() |
Posiciona después del último registro | rs.afterLast() |
absolute(int row) |
Va a una fila concreta | rs.absolute(3) |
relative(int rows) |
Se mueve relativo a la posición actual | rs.relative(2) |
Los métodos para obtener valores
| Método | Tipo devuelto | Ejemplo |
|---|---|---|
getInt() |
entero | rs.getInt("id") |
getString() |
String | rs.getString("nombre") |
getDouble() |
double | rs.getDouble("precio") |
getBoolean() |
boolean | rs.getBoolean("activo") |
getDate() |
Date | rs.getDate("fecha") |
getFloat() |
float | rs.getFloat("nota") |
getLong() |
long | rs.getLong("telefono") |
getObject() |
Object | rs.getObject("campo") |
Métodos para comprobar estado
| Método | Descripción |
|---|---|
isBeforeFirst() |
Indica si está antes del primer registro |
isAfterLast() |
Indica si está después del último |
isFirst() |
Indica si está en el primer registro |
isLast() |
Indica si está en el último registro |
wasNull() |
Comprueba si el último valor leído era NULL |
Un ResultSet puede abrirse para poder modificar datos directamente. Los métodos que tenemos para poder modificar
| Método | Descripción |
|---|---|
updateString() |
Actualiza un valor String |
updateInt() |
Actualiza un entero |
updateDouble() |
Actualiza un double |
updateRow() |
Guarda los cambios |
deleteRow() |
Elimina la fila actual |
insertRow() |
Inserta una nueva fila |
Ejemplo de actualización
rs.updateString("nombre", "Juan");
rs.updateRow();
Sentencias parametrizadas: PreparedStatement¶
PreparedStatement se utiliza para ejecutar consultas SQL precompiladas. En lugar de enviar una cadena de texto cruda a la base de datos, se envía una plantilla con parámetros identificados por ?.
String sql = """
INSERT INTO productos
(codigo, nombre, precio, categoria, stock, fecha_alta)
VALUES (?, ?, ?, ?, ?, CURRENT_DATE)
""";
Tiene como ventajas(las 3 eses):
- Seguridad (Anti SQL Injection): Al usar parámetros ?, el driver escapa automáticamente los caracteres peligrosos. Es imposible que un usuario inyecte código malicioso.
- Speed (Velocidad): La base de datos compila y optimiza el plan de ejecución de la consulta una sola vez y lo reutiliza, lo cual es mucho más rápido en ejecuciones repetitivas.
- Sintaxis Limpia: Evita la pesadilla de concatenar Strings con comillas simples y dobles
Las sentencias van a ser como las de SQL pero aquellos datos que se sustituirá por un parámetro lo indicamos con ?. Lo métodos son los siguientes
| Método | Devuelve | Se usa para | Descripción | Ejemplo típico con PreparedStatement |
|---|---|---|---|---|
executeQuery() |
ResultSet |
SELECT |
Ejecuta consultas que devuelven datos | SELECT * FROM productos WHERE precio < ? |
executeUpdate() |
int (filas afectadas) |
INSERT, UPDATE, DELETE |
Ejecuta sentencias que modifican datos | UPDATE productos SET precio = ? WHERE id = ? |
execute() |
boolean |
SQL genérico o dinámico | Ejecuta cualquier sentencia SQL; devuelve true si hay ResultSet |
CREATE TABLE IF NOT EXISTS productos (id INT, nombre VARCHAR(50)) |
De esta forma, tenemos una serie de métodos para insertar datos
| Método | Tipo de dato enviado | Usar para | Ejemplo |
|---|---|---|---|
ps.setInt(index, valor) |
int |
Enviar números enteros | ps.setInt(1, 10); |
ps.setString(index, valor) |
String |
Enviar texto o cadenas | ps.setString(2, "Teclado"); |
ps.setDouble(index, valor) |
double |
Enviar números decimales | ps.setDouble(3, 25.99); |
ps.setDate(index, java.sql.Date) |
Date |
Enviar fechas a la base de datos | ps.setDate(4, Date.valueOf("2025-03-01")); |
ps.setNull(index, java.sql.Types.INTEGER) |
NULL |
Enviar un valor NULL explícito |
ps.setNull(5, java.sql.Types.INTEGER); |
Importante
El índice de los parámetros ? empieza en 1, no en 0.
Ejemplo de sentencia Insert parametrizada
// Datos a insertar
String nombre = "Auriculares Sony WH-1000XM5";
double precio = 349.99;
String categoria = "Audio";
int stock = 18;
String codigo = "SONY-XM5-2024";
String sql = """
INSERT INTO productos
(codigo, nombre, precio, categoria, stock, fecha_alta)
VALUES (?, ?, ?, ?, ?, CURRENT_DATE)
""";
try (Connection conn = DriverManager.getConnection(url, usuario, clave);
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
// 1. Establecemos los parámetros (índices comienzan en 1)
ps.setString(1, codigo);
ps.setString(2, nombre);
ps.setDouble(3, precio);
ps.setString(4, categoria);
ps.setInt(5, stock);
// el sexto parámetro (fecha_alta) ya está definido como CURRENT_DATE en la consulta
// 2. Ejecutamos la inserción
int filasAfectadas = ps.executeUpdate();
System.out.println("Filas insertadas: " + filasAfectadas);
// 3. Recuperamos el ID autogenerado (si la tabla tiene AUTO_INCREMENT)
try (ResultSet claves = ps.getGeneratedKeys()) {
if (claves.next()) {
long idGenerado = claves.getLong(1);
System.out.println("→ ID del nuevo producto: " + idGenerado);
}
}
} catch (SQLException e) {
System.err.println("Error al insertar producto:");
System.err.println(e.getMessage());
e.printStackTrace();
}
// Parámetros de búsqueda (podrían venir de un formulario, API, etc.)
String categoriaBuscada = "Electrónica";
double precioMaximo = 450.00;
int stockMinimo = 10;
String sql = """
SELECT id, codigo, nombre, precio, stock, fecha_alta
FROM productos
WHERE categoria = ?
AND precio <= ?
AND stock >= ?
ORDER BY precio DESC
LIMIT 12
""";
try (Connection conn = DriverManager.getConnection(url, usuario, clave);
PreparedStatement ps = conn.prepareStatement(sql)) {
// 1. Asignamos valores a los parámetros (en el orden que aparecen en la consulta)
ps.setString(1, categoriaBuscada);
ps.setDouble(2, precioMaximo);
ps.setInt(3, stockMinimo);
// 2. Ejecutamos la consulta → devuelve ResultSet
try (ResultSet rs = ps.executeQuery()) {
int contador = 0;
while (rs.next()) {
contador++;
int id = rs.getInt("id");
String codigo = rs.getString("codigo");
String nombre = rs.getString("nombre");
double precio = rs.getDouble("precio");
int stock = rs.getInt("stock");
Date fecha = rs.getDate("fecha_alta");
System.out.printf("%4d | %-18s | %-35s | %8.2f | %5d | %s%n",
id, codigo, nombre, precio, stock,
fecha != null ? fecha : "—");
}
System.out.println("───────────────────────────────────────────────────────────────");
System.out.println("Total encontrados: " + contador);
}
} catch (SQLException e) {
System.err.println("Error en la consulta:");
System.err.println(e.getMessage());
e.printStackTrace();
}
Liberación de recursos¶
Danger 😬
Se debe cerrar explícitamente Statement, ResultSet y Connection cuando ya no se necesiten, a menos que se declaren con un try-catch-with-resources.
Las instancias de Connection y las de Statement almacenan, en memoria, mucha información relacionada con las ejecuciones realizadas. Además, mientras permanecen activas mantienen en el SGBD un conjunto importante de recursos abiertos, destinados a servir de forma eficiente las peticiones de los clientes. El cierre de estos objetos permite liberar recursos tanto del cliente como del servidor.
Aunque se haya cerrado la conexión, los objetos Statements que no se habían cerrado expresamente permanecen más tiempo en memoria que los objetos cerrados previamente, ya que el garbage collector de Java deberá hacer más comprobaciones para asegurar que ya no dispone de dependencias ni internas ni externas y se puede eliminar.
Es por ello que se recomienda proceder siempre a cerrarlo manualmente utilizando el método close(). El cierre de los objetos Statement asegura la liberación inmediata de los recursos y la anulación de las dependencias.
Importante 😵💫
Si en un mismo método queremos cerrar un objeto Statement y Connection, lo haremos siguiendo estos pasos:
-
Cerramos el
Statement -
Cerramos la instancia
Connection.
Si lo hiciéramos al revés, cuando intentáramos cerrar el Statement nos saltaría una excepción de tipo SQLException, ya que el cierre de la conexión lo habría dejado inaccesible.
Cuando se cierra un objeto Statement, su objeto ResultSet actual, si existe, también se cierra. Pero eso no ocurre cuando se cierra la conexión.
try (Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()) {
try (ResultSet resultSet = statement.executeQuery("SELECT * FROM ....")) {
// Do actions.
}
}
SQLException¶
🤓 SQLException: Es la excepción que se lanza cuando hay algún problema entre la base de datos y el programa Java JDBC. Contiene los siguientes métodos:
.getMessage(), nos indica la descripción del mensaje de error..getSQLState(), devuelve un código SQL estándar definido por ISO/ANSI y el Open Group que identifica de forma unívoca el error que se ha producido. SQLState Official.getErrorCode(), es un código de error que lanza la base de datos. En este caso el código de error es diferente dependiendo del proveedor de base de datos que estemos utilizando..getCause(), nos devuelve una lista de objetos que han provocado el error..getNextException(), devuelve la cadena de excepciones que se ha producido. De tal manera que podemos navegar sobre ella para ver en detalle de esas excepciones.
public void insertPersona(Persona persona) throws SQLException {
//preparamos la sentecia SQL en la que podemos sustituir los ? por valores
String sql = "INSERT INTO personas (dni, nombre, apellido, edad) VALUES (?, ?, ?, ?)";
try (PreparedStatement statement = connection.prepareStatement(INSERT_QUERY)) {
//sustituye el valor por el primer ?
statement.setString(1, persona.getDni());
//sustituye el valor por el segundo ?
statement.setString(2, persona.getNombre());
statement.setString(3, persona.getApellido());
statement.setInt(4, persona.getEdad());
statement.executeUpdate();
} catch (SQLException e) {
// Código de error 1062 corresponde a clave duplicada en MySQL
if (e.getSQLState().equals("23000") || e.getErrorCode() == 1062) {
System.err.println("Error: Clave duplicada para el DNI " + persona.getDni());
} else {
throw e; // Relanza la excepción si es otro error
}
}
}
Ataque por Inyección SQL¶
La inyección SQL es un tipo de ataque en el que un atacante inserta código SQL malicioso en las consultas SQL de una aplicación. Un ejemplo simplificado de cómo podría ocurrir una inyección SQL en Java:
Si tenemos un método para autenticar el usuario en el que construimos la sentencia SQL por concatenación
// Método para autenticar usuarios
public boolean autenticarUsuario(Connection conexion, String nombreUsuario, String contraseña) throws SQLException {
String consulta = "SELECT * FROM Usuarios WHERE nombre = '"
+ nombreUsuario
+"' AND contraseña = '"
+ contraseña + "'";
PreparedStatement statement = conexion.prepareStatement(consulta);
ResultSet resultado = statement.executeQuery();
return resultado.next();
}
Un atacante podría explotar esta vulnerabilidad ingresando un nombre de usuario malicioso, como " ' OR '1'='1" y una contraseña arbitraria. Esto alteraría la consulta SQL de la siguiente manera:
SELECT * FROM Usuarios WHERE nombre = '' OR '1'='1' AND contraseña = 'contraseña';
OR '1'='1' siempre evalúa como verdadero, por lo que la consulta devolvería todos los registros de la tabla Usuarios, permitiendo que el atacante acceda a la cuenta de cualquier usuario sin necesidad de una contraseña válida
Para prevenir la inyección SQL, se recomienda utilizar consultas parametrizadas o consultas preparadas, que permiten pasar los parámetros de forma segura sin concatenación directa en la cadena SQL. Por ejemplo:
public boolean autenticarUsuario(Connection conexion, String nombreUsuario, String contraseña) throws SQLException {
String consulta = "SELECT * FROM Usuarios WHERE nombre = ? AND contraseña = ?";
PreparedStatement statement = conexion.prepareStatement(consulta);
statement.setString(1, nombreUsuario);
statement.setString(2, contraseña);
ResultSet resultado = statement.executeQuery();
return resultado.next();
}