Saltar a contenido

⚙️ 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...)

jdbc

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:

MySQL JDBC

Podemos añadirlo fácilmente en IntelliJ a nuestro proyecto

jdbc

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étodo getConnection().

  • 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 objetos Connection mantendrá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

alt text

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

alt text

  • 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

  • Statement y PreparedStatement: estas interfaces se utilizan para ejecutar consultas SQL estáticas y consultas SQL parametrizadas, respectivamente. Statement es la superinterfaz de la interfaz PreparedStatement, 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 un ResultSet, 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 objeto ResultSet que 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

alt text

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();
        }
El control de nulos en la base de datos es importante, ya que los tipos primitivos no aceptan nulos y es posible que quiera saber si hay un nulo.

//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();
        }
En las sentencia SQL de agregados donde contamos, hacemos medias, sumas...también podemos recuperar los datos
        // ------------------------------------------------------------------------
        // 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();
}
Ejemplo con sentencia SQL SELECT

// 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:

  1. Cerramos el Statement

  2. 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';
El 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();
}