Skip to content

Acceso a BD Relacionales

Piensa en JDBC como el cerebro traductor de una app Java: recibe una intención humana, la convierte en SQL y recoge la respuesta de la base de datos para devolvértela con orden y disciplina. Si lo haces bien, tu aplicación parece una orquesta; si lo haces mal, parece un grupo de WhatsApp a las tres de la mañana intentando ponerse de acuerdo.

Conceptos Fundamentales

Definición: Base de datos relacional

Es un sistema que guarda la información en tablas formadas por filas y columnas. Cada fila representa un registro y cada columna un atributo, como si fueran fichas bien ordenadas en una gran hoja de cálculo profesional.

Definición: SGBD

Un Sistema Gestor de Bases de Datos es el software que crea, administra y protege las bases de datos. Ejemplos típicos en DAW son MySQL, MariaDB, PostgreSQL y Oracle.

Definición: JDBC

Java Database Connectivity es la API estándar de Java para conectarse a bases de datos relacionales. Permite enviar SQL, leer resultados y controlar conexiones sin depender de un único motor.

Definición: Driver JDBC

Es la implementación concreta que entiende el protocolo de un SGBD determinado. JDBC es el enchufe universal; el driver es el adaptador que hace que el enchufe encaje en MySQL, MariaDB u Oracle.

Definición: Connection

Representa una sesión abierta con la base de datos. Desde ella se crean sentencias, se gestionan transacciones y se liberan recursos cuando termina el trabajo.

Definición: Statement

Objeto encargado de ejecutar sentencias SQL sobre una conexión. Puede ejecutar consultas, modificaciones y operaciones de administración según el método utilizado.

Definición: ResultSet

Es el conjunto de filas devuelto normalmente por un SELECT. Funciona como una tabla temporal con un cursor que va avanzando por los registros.

Definición: PreparedStatement

Es una sentencia SQL precompilada con marcadores ? para introducir valores después. Es la opción recomendada para parámetros y evita muchos problemas de seguridad.

Definición: DAO

Patrón que separa el acceso a datos del resto de la aplicación. En vez de mezclar SQL por todo el código, centralizas la lógica de persistencia en clases especializadas.

Desfase objeto-relacional

La API estándar de Java para hablar con bases de datos se llama JDBC (Java Database Connectivity). Es el "traductor universal".

Definición: Driver JDBC

Es una librería específica (un archivo .jar) creada por el fabricante de la base de datos (Oracle, MySQL, etc.) que enseña a la API JDBC de Java cómo hablar el dialecto exacto de esa base de datos concreta.

Cuando programas en Java, piensas en objetos: Cliente, Pedido, Factura, Producto. Pero la base de datos piensa en tablas, filas y columnas. Ese choque de mentalidades se llama desfase objeto-relacional y es una de las razones por las que existe una capa de acceso a datos. En otras palabras: Java habla en objetos, la BD habla en filas, y JDBC hace de intérprete simultáneo.

public class Cliente {
    private long id;
    private String nombre;
    private String ciudad;

    public Cliente(long id, String nombre, String ciudad) {
        this.id = id;
        this.nombre = nombre;
        this.ciudad = ciudad;
    }

    public long getId() { return id; }
    public String getNombre() { return nombre; }
    public String getCiudad() { return ciudad; }
}
// Transformación manual de una fila a un objeto
Cliente cliente = new Cliente(
    rs.getLong("id"),
    rs.getString("nombre"),
    rs.getString("ciudad")
);
// Transformación inversa: de objeto a parámetros SQL
PreparedStatement ps = conn.prepareStatement(
    "INSERT INTO clientes(nombre, ciudad) VALUES (?, ?)"
);
ps.setString(1, cliente.getNombre());
ps.setString(2, cliente.getCiudad());
Mundo Java Mundo relacional
Objeto Tabla no, fila sí
Atributo Columna
Colección Conjunto de registros
Herencia No existe de forma natural
Métodos No se almacenan

Tip

Si el objeto es la “ficha de personaje” y la tabla es el “inventario”, JDBC es el sistema de guardado de la partida. Sin él, perderías progreso cada vez que cierras la app.

Warning

No intentes “guardar objetos” directamente en tablas sin pensar en el mapeo. El resultado suele ser un diseño poco flexible y un montón de campos que nadie entiende seis meses después.

// Diseño claro
public class Pedido {
    private long id;
    private LocalDate fecha;
    private BigDecimal total;
}
// Mapeo a columnas
String sql = "SELECT id, fecha, total FROM pedidos WHERE id = ?";
PreparedStatement ps = conn.prepareStatement(sql);

Question

  1. ¿Por qué una base de datos relacional no puede almacenar un objeto Java tal cual?
  2. ¿Qué ventajas tiene separar el mapeo de datos en una clase DAO?
  3. ¿Qué problemas aparecen cuando la lógica SQL se dispersa por toda la aplicación?
  4. ¿Qué relación hay entre un objeto Java y una fila de una tabla?



Protocolos y drivers

Antes de JDBC, ODBC fue una forma popular de acceder a datos desde distintas aplicaciones, pero no está tan alineado con Java y su filosofía de portabilidad. JDBC nace para Java y se apoya en interfaces y clases estandarizadas, mientras que cada base de datos aporta su driver concreto. El resumen simple es: JDBC unifica, el driver especializa.

// JDBC: URL común para el tipo de conexión
String url = "jdbc:mysql://localhost:3306/tienda";
Connection conn = DriverManager.getConnection(url, "root", "clave");
// El mismo patrón de conexión vale para otros SGBD
String urlPg = "jdbc:postgresql://localhost:5432/tienda";
String urlOracle = "jdbc:oracle:thin:@localhost:1521:xe";
// Conexión a una BD autocontenida como SQLite
String urlPg = "jdbc:sqlite:alumnos.sqlite";
// Carga dinámica del driver
Class.forName("com.mysql.cj.jdbc.Driver");
Tecnología Filosofía Encaje con Java Portabilidad
ODBC Generalista, nativa Menor Media
JDBC Específica para Java Muy alta Alta
Driver tipo 4 Pure Java Excelente Muy alta
Driver tipo 1-3 Más dependencias Menor Variable

Actualidad

MySQL Connector/J sigue siendo un driver de tipo 4, es decir, pure Java y sin depender de librerías nativas del cliente.

Danger

No mezcles “driver” con “base de datos”. El driver no es el servidor; es el puente. Si el puente falla, no es culpa del coche, es culpa de la carretera o del peaje.

// Ejemplo conceptual de separación
public interface ConexionFactory {
    Connection getConnection() throws SQLException;
}
public class MySqlConexionFactory implements ConexionFactory {
    @Override
    public Connection getConnection() throws SQLException {
        return DriverManager.getConnection(
            "jdbc:mysql://localhost:3306/tienda",
            "root",
            "clave"
        );
    }
}

Question

  1. ¿Qué aporta JDBC frente a una solución dependiente del sistema operativo?
  2. ¿Qué es exactamente lo que cambia si migras de MySQL a PostgreSQL?
  3. ¿Por qué los drivers tipo 4 son tan usados hoy?
  4. ¿Qué pasaría si tu aplicación dependiera de una librería nativa en cada cliente?



Componentes Clave de JDBC

Componente Responsabilidad
DriverManager El "jefe" que gestiona los drivers y establece las conexiones.
Connection La sesión abierta con la base de datos. ¡No la dejes abierta o el servidor llorará!
Statement El sobre donde metes tu carta (SQL) para enviarla.
PreparedStatement Un Statement precompilado. Más rápido y seguro contra inyecciones SQL.
ResultSet La caja que contiene las filas que te devuelve la base de datos.
SQLException La clase que maneja cualquier error que ocurra en la base de datos.
graph TD
    subgraph "Aplicación Java"
        A[Código del Alumno] --> B[DriverManager]
        B --> C[Connection]
        C --> D[Statement / PreparedStatement]
    end

    subgraph "Capa de Datos"
        E[Driver JDBC MySQL]
        F[(Base de Datos MySQL)]
    end

    D --> E
    E --> F
    F --> E
    E --> D
    D --> G[ResultSet]
    G --> A
sequenceDiagram
participant App as Aplicación
participant DM as DriverManager
participant Conn as Connection
participant St as Statement
participant DB as SGBD

App->>DM: getConnection(url, user, pass)
DM-->>App: Connection
App->>Conn: createStatement()
Conn-->>App: Statement
App->>St: executeQuery("SELECT ...")
St->>DB: SQL
DB-->>St: filas
St-->>App: ResultSet
// Estructura mínima realista
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/tienda?serverTimezone=UTC",
    "root",
    "clave"
);
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT id, nombre FROM clientes");
// Cierre de recursos
rs.close();
st.close();
conn.close();

Tip

La arquitectura no es solo teoría. Entenderla te ayuda a detectar fallos: si falla el driver, no mires antes de tiempo la consulta SQL; si falla la conexión, no culpes al ResultSet.

Warning

En MySQL es habitual añadir serverTimezone=UTC si aparece un error de zona horaria al conectar. Es una de esas pequeñas cosas que parecen invisibles… hasta que te arruinan la mañana.

String url = "jdbc:mysql://localhost:3306/tienda?serverTimezone=UTC";
Connection conn = DriverManager.getConnection(url, "root", "clave");

Question

  1. ¿Qué papel juega DriverManager en el arranque de una conexión?
  2. ¿Qué objeto se encarga de ejecutar realmente la sentencia SQL?
  3. ¿Por qué ResultSet no es una lista normal de Java?
  4. ¿Qué ventaja tiene conocer la arquitectura antes de programar?

Statement, PreparedStatement y ResultSet

Statement

Statement es la herramienta más directa para enviar SQL a la base de datos. Se usa bien cuando la sentencia es fija, corta y no necesita parámetros, como una consulta administrativa o una prueba rápida en clase.

try (Connection conn = DriverManager.getConnection(url, user, pass);
     Statement st = conn.createStatement();
     ResultSet rs = st.executeQuery("SELECT id, nombre, ciudad FROM clientes")) {

    while (rs.next()) {
        long id = rs.getLong("id");
        String nombre = rs.getString("nombre");
        String ciudad = rs.getString("ciudad");
        System.out.println(id + " | " + nombre + " | " + ciudad);
    }
}
try (Connection conn = DriverManager.getConnection(url, user, pass);
     Statement st = conn.createStatement()) {

    int filas = st.executeUpdate(
        "UPDATE clientes SET ciudad = 'León' WHERE id = 10"
    );
    System.out.println("Filas afectadas: " + filas);
}

Statement tiene tres usos muy típicos: executeQuery para lecturas, executeUpdate para inserciones, modificaciones y borrados, y execute para casos más genéricos o procedimientos que puedan devolver varios resultados. Su ventaja es la simplicidad; su desventaja es que, si lo usas mal con datos externos, puedes abrir la puerta a inyección SQL.

Danger

No construyas SQL concatenando texto que venga del usuario. Si el usuario controla parte de la consulta, también puede controlar el desastre.

// Mala idea
String sql = "SELECT * FROM clientes WHERE nombre = '" + nombre + "'";
// Mejor opción
String sql = "SELECT * FROM clientes WHERE nombre = ?";

PreparedStatement

PreparedStatement es la versión profesional de la misma idea. La consulta se define con ? y luego se rellenan los valores con métodos setXxx, lo que hace el código más seguro, más legible y más reutilizable.

String sql = "INSERT INTO clientes(nombre, ciudad, edad) VALUES (?, ?, ?)";

try (Connection conn = DriverManager.getConnection(url, user, pass);
     PreparedStatement ps = conn.prepareStatement(sql)) {

    ps.setString(1, "Ana");
    ps.setString(2, "León");
    ps.setInt(3, 21);

    int filas = ps.executeUpdate();
    System.out.println("Insertadas: " + filas);
}
String sql = "SELECT id, nombre FROM clientes WHERE ciudad = ? AND edad >= ?";

try (Connection conn = DriverManager.getConnection(url, user, pass);
     PreparedStatement ps = conn.prepareStatement(sql)) {

    ps.setString(1, "León");
    ps.setInt(2, 18);

    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getLong("id") + " " + rs.getString("nombre"));
        }
    }
}
String sql = "DELETE FROM clientes WHERE id = ?";

try (Connection conn = DriverManager.getConnection(url, user, pass);
     PreparedStatement ps = conn.prepareStatement(sql)) {

    ps.setLong(1, 10L);
    ps.executeUpdate();
}
Aspecto Statement PreparedStatement
Parámetros No
Seguridad Más delicado Mejor frente a inyección SQL
Reutilización Baja Alta
Legibilidad Correcta Mejor
Uso recomendado SQL fijo SQL con datos variables

Tip

PreparedStatement es como una plantilla de currículum: el formato ya está decidido, solo cambias nombre, experiencia y puesto. Mucho más limpio que reescribir todo cada vez.

ResultSet

ResultSet contiene los datos devueltos por una consulta, normalmente un SELECT. Su idea es sencilla: el cursor empieza antes de la primera fila y avanza con next() hasta que ya no quedan registros.

String sql = "SELECT id, nombre, ciudad FROM clientes ORDER BY nombre";

try (Connection conn = DriverManager.getConnection(url, user, pass);
     Statement st = conn.createStatement();
     ResultSet rs = st.executeQuery(sql)) {

    while (rs.next()) {
        long id = rs.getLong("id");
        String nombre = rs.getString("nombre");
        String ciudad = rs.getString("ciudad");

        System.out.println(id + " - " + nombre + " - " + ciudad);
    }
}
try (Connection conn = DriverManager.getConnection(url, user, pass);
     Statement st = conn.createStatement();
     ResultSet rs = st.executeQuery("SELECT id, edad FROM clientes")) {

    while (rs.next()) {
        long id = rs.getLong(1);
        int edad = rs.getInt(2);
        System.out.println("ID=" + id + ", edad=" + edad);
    }
}
try (Connection conn = DriverManager.getConnection(url, user, pass);
     Statement st = conn.createStatement();
     ResultSet rs = st.executeQuery("SELECT nombre, ciudad FROM clientes")) {

    if (rs.next()) {
        System.out.println("Primer cliente: " + rs.getString("nombre"));
    }
}

Los getters de ResultSet convierten tipos SQL a tipos Java cuando es posible: getString, getInt, getDouble, getDate, getBoolean, etc. Si intentas recuperar un tipo incompatible, el driver lanzará una excepción. También puedes acceder por nombre de columna o por índice; por claridad, en código real suele ser mejor usar el nombre.

Warning

Las columnas empiezan en 1, no en 0. Ese detalle ha hecho sufrir a más de un alumno que venía confiado desde arrays y listas.

Con el tipo por defecto, el cursor solo avanza hacia delante. Pero JDBC permite crear ResultSet desplazables, que pueden moverse al principio, al final o a una posición concreta.

Statement st = conn.createStatement(
    ResultSet.TYPE_SCROLL_INSENSITIVE,
    ResultSet.CONCUR_READ_ONLY
);

ResultSet rs = st.executeQuery("SELECT id, nombre FROM clientes");

rs.last();
System.out.println("Último registro: " + rs.getString("nombre"));

rs.first();
System.out.println("Primer registro: " + rs.getString("nombre"));

rs.absolute(3);
System.out.println("Tercer registro: " + rs.getString("nombre"));
Statement st = conn.createStatement(
    ResultSet.TYPE_SCROLL_SENSITIVE,
    ResultSet.CONCUR_UPDATABLE
);

ResultSet rs = st.executeQuery("SELECT id, nombre, ciudad FROM clientes");

if (rs.first()) {
    System.out.println("Primero: " + rs.getString("nombre"));
}
if (rs.isBeforeFirst()) {
    System.out.println("El cursor está antes del primer registro");
}

Si el ResultSet es sensible, puede reflejar cambios en la base de datos mientras sigue abierto. Si es insensible, se comporta como una instantánea de los datos en el momento de la consulta. Para aprender y depurar, los desplazables son muy útiles; para código sencillo, el next() básico suele ser suficiente.

Question

  1. ¿Qué diferencia real hay entre executeQuery y executeUpdate?
  2. ¿Por qué PreparedStatement es mejor para datos introducidos por el usuario?
  3. ¿Cuándo te interesa un ResultSet desplazable?
  4. ¿Por qué usar índices en ResultSet puede hacer el código menos claro?



Tipos de sentencias SQL

DDL

Las sentencias DDL definen la estructura de la base de datos. Aquí entran comandos como CREATE, ALTER y DROP, que crean, modifican o eliminan objetos como tablas, vistas e índices.

try (Connection conn = DriverManager.getConnection(url, user, pass);
     Statement st = conn.createStatement()) {

    st.executeUpdate("""
        CREATE TABLE productos (
            id INT PRIMARY KEY,
            nombre VARCHAR(100) NOT NULL,
            precio DECIMAL(10,2) NOT NULL
        )
    """);
}
try (Connection conn = DriverManager.getConnection(url, user, pass);
     Statement st = conn.createStatement()) {

    st.executeUpdate("ALTER TABLE productos ADD categoria VARCHAR(50)");
}
try (Connection conn = DriverManager.getConnection(url, user, pass);
     Statement st = conn.createStatement()) {

    st.executeUpdate("DROP TABLE productos");
}

DML

Las sentencias DML trabajan sobre los datos: INSERT, UPDATE, DELETE y, según el contexto, algunas consultas de recuperación. Son el pan de cada día cuando la aplicación gestiona información de negocio real.

try (Connection conn = DriverManager.getConnection(url, user, pass);
     PreparedStatement ps = conn.prepareStatement(
         "INSERT INTO productos(nombre, precio) VALUES (?, ?)"
     )) {

    ps.setString(1, "Teclado mecánico");
    ps.setBigDecimal(2, new java.math.BigDecimal("79.99"));
    ps.executeUpdate();
}
try (Connection conn = DriverManager.getConnection(url, user, pass);
     PreparedStatement ps = conn.prepareStatement(
         "UPDATE productos SET precio = ? WHERE id = ?"
     )) {

    ps.setBigDecimal(1, new java.math.BigDecimal("69.99"));
    ps.setInt(2, 5);
    ps.executeUpdate();
}
try (Connection conn = DriverManager.getConnection(url, user, pass);
     PreparedStatement ps = conn.prepareStatement(
         "DELETE FROM productos WHERE id = ?"
     )) {

    ps.setInt(1, 8);
    ps.executeUpdate();
}
Tipo Qué hace Ejemplo
DDL Define estructura CREATE TABLE
DML Modifica datos INSERT, UPDATE, DELETE
Consulta Recupera datos SELECT
Metadatos Describe la BD DatabaseMetaData

Tip

En bases de datos reales, la creación de tablas normalmente la hace el equipo técnico, no el usuario final. Si el usuario puede lanzar DROP TABLE, el susto no es “si”, sino “cuándo”.

Transacciones

Cuando varias operaciones deben hacerse como una sola unidad, necesitas transacciones. Si una parte falla, puedes deshacer todo con rollback; si todo va bien, guardas con commit. Es el equivalente a hacer una transferencia bancaria: o se completa entera o no debería dejarte el dinero “a medias”.

try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    conn.setAutoCommit(false);

    try (PreparedStatement ps1 = conn.prepareStatement(
             "UPDATE cuentas SET saldo = saldo - ? WHERE id = ?"
         );
         PreparedStatement ps2 = conn.prepareStatement(
             "UPDATE cuentas SET saldo = saldo + ? WHERE id = ?"
         )) {

        ps1.setBigDecimal(1, new java.math.BigDecimal("100.00"));
        ps1.setInt(2, 1);
        ps1.executeUpdate();

        ps2.setBigDecimal(1, new java.math.BigDecimal("100.00"));
        ps2.setInt(2, 2);
        ps2.executeUpdate();

        conn.commit();
    } catch (SQLException e) {
        conn.rollback();
        throw e;
    }
}
try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    conn.setAutoCommit(false);
    // operaciones
    conn.commit();
}
try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    conn.setAutoCommit(false);
    // algo falla
    conn.rollback();
}

Danger

Si desactivas autoCommit y luego se te olvida hacer commit, tus cambios pueden desaparecer. Es como editar un documento y cerrar sin guardar: el drama es conocido y universal.

Question

  1. ¿Qué diferencia hay entre DDL y DML?
  2. ¿Por qué las transacciones son tan importantes en banca, stock o pedidos?
  3. ¿Qué ocurre si una operación falla dentro de una transacción?
  4. ¿Cuándo conviene desactivar autoCommit?

Meta datos y exploración

DatabaseMetaData

DatabaseMetaData sirve para descubrir cómo es la base de datos sin conocerla de antemano. Es muy útil en herramientas de administración, generadores de documentación y utilidades de diagnóstico.

try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    DatabaseMetaData md = conn.getMetaData();

    System.out.println(md.getDatabaseProductName());
    System.out.println(md.getDatabaseProductVersion());
    System.out.println(md.getDriverName());
    System.out.println(md.getURL());
}
try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    DatabaseMetaData md = conn.getMetaData();
    try (ResultSet rs = md.getTables(null, null, "%", new String[]{"TABLE"})) {
        while (rs.next()) {
            System.out.println(rs.getString("TABLE_NAME"));
        }
    }
}
try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    DatabaseMetaData md = conn.getMetaData();
    try (ResultSet rs = md.getColumns(null, null, "clientes", null)) {
        while (rs.next()) {
            System.out.println(
                rs.getString("COLUMN_NAME") + " | " +
                rs.getString("TYPE_NAME") + " | " +
                rs.getInt("COLUMN_SIZE")
            );
        }
    }
}
String[] tipos = {"TABLE", "VIEW"};
ResultSet rs = md.getTables(null, "tienda", "%", tipos);

getTables y getColumns permiten filtrar por catálogo, esquema y patrón de nombres. En MySQL y MariaDB, los catálogos y esquemas pueden comportarse de forma particular, así que conviene probar con cuidado en el entorno real. Esta clase es una navaja suiza para inspeccionar la BD sin depender del diseño exacto de cada aplicación.

ResultSetMetaData

ResultSetMetaData describe las columnas de un resultado concreto. Es útil cuando quieres construir tablas dinámicas, exportar datos o hacer depuración avanzada.

try (Connection conn = DriverManager.getConnection(url, user, pass);
     Statement st = conn.createStatement();
     ResultSet rs = st.executeQuery("SELECT id, nombre, ciudad FROM clientes")) {

    ResultSetMetaData md = rs.getMetaData();
    int columnas = md.getColumnCount();

    for (int i = 1; i <= columnas; i++) {
        System.out.println(md.getColumnName(i) + " -> " + md.getColumnTypeName(i));
    }
}
int cols = rs.getMetaData().getColumnCount();
String etiqueta = rs.getMetaData().getColumnLabel(2);

Tip

ResultSetMetaData es como leer la cabecera de una tabla Excel antes de procesarla. Así sabes qué estás leyendo antes de hacer números raros.

Question

  1. ¿Qué diferencia hay entre DatabaseMetaData y ResultSetMetaData?
  2. ¿Para qué sirve getTables?
  3. ¿Qué utilidad tiene getColumnCount?
  4. ¿Por qué estas clases son útiles en herramientas genéricas?



DAO y Singleton

Singleton

Crear una conexión a la base de datos es costoso en tiempo y recursos. El patrón Singleton nos asegura que solo haya una instancia de un objeto, pero para conexiones, lo ideal es un Pool de Conexiones. Un pool mantiene varias conexiones abiertas y listas para usar, lo que mejora drásticamente el rendimiento. Para este ejemplo, simularemos un Singleton simple para la conexión, pero en producción usaríamos librerías como HikariCP o c3p0.

classDiagram
    class ConexionBD {
        -static Connection instance
        -ConexionBD()
        +static Connection getConnection()
        +static void closeConnection()
    }
    note for ConexionBD "Asegura una única instancia de conexión\n(o simula un pool básico)"

Ejemplo 11: Implementación del Singleton para la Conexión

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConexionBD {
    private static Connection instance = null;
    private final static String URL = "jdbc:mysql://localhost:3306/gestionalumnos?serverTimezone=UTC&useSSL=false";
    private final static String USER = "profesor";
    private final static String PASS = "12345678";

    // Constructor privado para evitar instanciación externa
    private ConexionBD() {
        try {
            instance = DriverManager.getConnection(URL, USER, PASS);
            System.out.println("Conexión a la BD establecida (Singleton).");
        } catch (SQLException e) {
            System.err.println("Error al establecer la conexión Singleton: " + e.getMessage());
            e.printStackTrace();
        }
    }

    // Método estático para obtener la única instancia de la conexión
    public static Connection getConnection() {
        if (instance == null) {
            new ConexionBD(); // Crea la instancia si no existe
        }
        return instance;
    }

    // Método para cerrar la conexión (importante en aplicaciones pequeñas)
    public static void closeConnection() {
        if (instance != null) {
            try {
                instance.close();
                System.out.println("Conexión a la BD cerrada (Singleton).");
            } catch (SQLException e) {
                System.err.println("Error al cerrar la conexión Singleton: " + e.getMessage());
            }
        }
    }
}

Cuando una aplicación usa la base de datos muchas veces, no conviene crear conexiones sin control. El patrón Singleton permite centralizar la creación y reutilización de la conexión, con cuidado de no convertirlo en un “monstruo global”.

public final class ConexionBD {
    private static Connection conn;
    private static final String URL = "jdbc:mysql://localhost:3306/tienda?serverTimezone=UTC";
    private static final String USER = "root";
    private static final String PASS = "clave";

    private ConexionBD() {}

    public static Connection getInstance() throws SQLException {
        if (conn == null || conn.isClosed()) {
            conn = DriverManager.getConnection(URL, USER, PASS);
        }
        return conn;
    }
}
Connection conn1 = ConexionBD.getInstance();
Connection conn2 = ConexionBD.getInstance();
System.out.println(conn1 == conn2);

DAO

El patrón DAO abstrae la lógica de acceso a datos de la lógica de negocio. Esto significa que tu aplicación no necesita saber cómo se guardan o recuperan los datos, solo le pide al DAO que lo haga. Usaremos interfaces genéricas para que el DAO sea reutilizable para diferentes entidades (Alumno, Profesor, Asignatura, etc.).

Ejemplo 12: Interfaz Genérica IOperationsCRUD

import java.util.List;

public interface IOperationsCRUD<T, ID> {
    T save(T entity);
    T findById(ID id);
    List<T> findAll();
    T update(T entity);
    void delete(ID id);
}

Ejemplo 13: Clase POJO Alumno

public class Alumno {
    private int id;
    private String nombre;
    private String apellido1;
    private String apellido2;
    private String ciudad;
    private int categoria;

    public Alumno(int id, String nombre, String apellido1, String apellido2, String ciudad, int categoria) {
        this.id = id;
        this.nombre = nombre;
        this.apellido1 = apellido1;
        this.apellido2 = apellido2;
        this.ciudad = ciudad;
        this.categoria = categoria;
    }

    // Constructor sin ID para inserciones
    public Alumno(String nombre, String apellido1, String apellido2, String ciudad, int categoria) {
        this(0, nombre, apellido1, apellido2, ciudad, categoria);
    }

    // Getters y Setters
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getNombre() { return nombre; }
    public void setNombre(String nombre) { this.nombre = nombre; }
    public String getApellido1() { return apellido1; }
    public void setApellido1(String apellido1) { this.apellido1 = apellido1; }
    public String getApellido2() { return apellido2; }
    public void setApellido2(String apellido2) { this.apellido2 = apellido2; }
    public String getCiudad() { return ciudad; }
    public void setCiudad(String ciudad) { this.ciudad = ciudad; }
    public int getCategoria() { return categoria; }
    public void setCategoria(int categoria) { this.categoria = categoria; }

    @Override
    public String toString() {
        return "Alumno{" +
               "id=" + id +
               ", nombre=\'" + nombre + "\'" +
               ", apellido1=\'" + apellido1 + "\'" +
               ", apellido2=\'" + apellido2 + "\'" +
               ", ciudad=\'" + ciudad + "\'" +
               ", categoria=" + categoria +
               '\}';
    }
}

Ejemplo 14: Implementación AlumnoDAOImpl

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class AlumnoDAOImpl implements IOperationsCRUD<Alumno, Integer> {

    @Override
    public Alumno save(Alumno alumno) {
        String sql = "INSERT INTO alumno (nombre, apellido1, apellido2, ciudad, categoria) VALUES (?, ?, ?, ?, ?)";
        try (Connection conn = ConexionBD.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {

            pstmt.setString(1, alumno.getNombre());
            pstmt.setString(2, alumno.getApellido1());
            pstmt.setString(3, alumno.getApellido2());
            pstmt.setString(4, alumno.getCiudad());
            pstmt.setInt(5, alumno.getCategoria());

            int affectedRows = pstmt.executeUpdate();
            if (affectedRows > 0) {
                try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
                    if (generatedKeys.next()) {
                        alumno.setId(generatedKeys.getInt(1)); // Asignar el ID generado
                    }
                }
            }
            return alumno;
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public Alumno findById(Integer id) {
        String sql = "SELECT id, nombre, apellido1, apellido2, ciudad, categoria FROM alumno WHERE id = ?";
        try (Connection conn = ConexionBD.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setInt(1, id);
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return new Alumno(
                        rs.getInt("id"),
                        rs.getString("nombre"),
                        rs.getString("apellido1"),
                        rs.getString("apellido2"),
                        rs.getString("ciudad"),
                        rs.getInt("categoria")
                    );
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public List<Alumno> findAll() {
        List<Alumno> alumnos = new ArrayList<>();
        String sql = "SELECT id, nombre, apellido1, apellido2, ciudad, categoria FROM alumno";
        try (Connection conn = ConexionBD.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            while (rs.next()) {
                alumnos.add(new Alumno(
                    rs.getInt("id"),
                    rs.getString("nombre"),
                    rs.getString("apellido1"),
                    rs.getString("apellido2"),
                    rs.getString("ciudad"),
                    rs.getInt("categoria")
                ));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return alumnos;
    }

    @Override
    public Alumno update(Alumno alumno) {
        String sql = "UPDATE alumno SET nombre = ?, apellido1 = ?, apellido2 = ?, ciudad = ?, categoria = ? WHERE id = ?";
        try (Connection conn = ConexionBD.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, alumno.getNombre());
            pstmt.setString(2, alumno.getApellido1());
            pstmt.setString(3, alumno.getApellido2());
            pstmt.setString(4, alumno.getCiudad());
            pstmt.setInt(5, alumno.getCategoria());
            pstmt.setInt(6, alumno.getId());

            int affectedRows = pstmt.executeUpdate();
            return (affectedRows > 0) ? alumno : null;
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public void delete(Integer id) {
        String sql = "DELETE FROM alumno WHERE id = ?";
        try (Connection conn = ConexionBD.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setInt(1, id);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Ejemplo 15: FactoriaDAO para gestionar instancias de DAO

public class FactoriaDAO {
    private static AlumnoDAO alumnoDAO = null;

    private FactoriaDAO() { /* Evitar instanciación */ }

    public static AlumnoDAO getAlumnoDAO() {
        if (alumnoDAO == null) {
            alumnoDAO = new AlumnoDAOImpl();
        }
        return alumnoDAO;
    }
    // Podríamos añadir más métodos para otros DAOs: getProfesorDAO(), etc.
}

Ejemplo 16: Uso del DAO en la aplicación principal

import java.util.List;

public class MainApp {
    public static void main(String[] args) {
        // Obtener la instancia del DAO a través de la factoría
        AlumnoDAO alumnoDAO = FactoriaDAO.getAlumnoDAO();

        // 1. Insertar un nuevo alumno
        Alumno nuevoAlumno = new Alumno("Ana", "Pérez", "López", "Barcelona", 1);
        alumnoDAO.save(nuevoAlumno);
        System.out.println("Alumno insertado: " + nuevoAlumno);

        // 2. Buscar un alumno por ID
        Alumno alumnoEncontrado = alumnoDAO.findById(nuevoAlumno.getId());
        System.out.println("Alumno encontrado: " + alumnoEncontrado);

        // 3. Actualizar un alumno
        if (alumnoEncontrado != null) {
            alumnoEncontrado.setCiudad("Valencia");
            alumnoDAO.update(alumnoEncontrado);
            System.out.println("Alumno actualizado: " + alumnoEncontrado);
        }

        // 4. Listar todos los alumnos
        List<Alumno> todosLosAlumnos = alumnoDAO.findAll();
        System.out.println("\n--- TODOS LOS ALUMNOS ---");
        todosLosAlumnos.forEach(System.out::println);

        // 5. Eliminar un alumno
        if (nuevoAlumno.getId() != 0) { // Asegurarse de que tiene un ID válido
            alumnoDAO.delete(nuevoAlumno.getId());
            System.out.println("Alumno con ID " + nuevoAlumno.getId() + " eliminado.");
        }

        // Cerrar la conexión al finalizar la aplicación (si se usa Singleton simple)
        ConexionBD.closeConnection();
    }
}

Consejo de Arquitectura

En aplicaciones más grandes, el FactoriaDAO podría ser reemplazado por un framework de Inyección de Dependencias como Spring, que gestiona automáticamente la creación y el ciclo de vida de los objetos DAO y las conexiones a la base de datos.

DAO concentra la lógica de acceso a datos en clases específicas. Es una forma limpia de esconder el SQL detrás de métodos más expresivos como findAll, findById, insert, update y delete.

public class ClienteDAO {
    public List<Cliente> findAll() throws SQLException {
        List<Cliente> clientes = new ArrayList<>();
        String sql = "SELECT id, nombre, ciudad FROM clientes ORDER BY nombre";

        try (Connection conn = ConexionBD.getInstance();
             Statement st = conn.createStatement();
             ResultSet rs = st.executeQuery(sql)) {

            while (rs.next()) {
                clientes.add(new Cliente(
                    rs.getLong("id"),
                    rs.getString("nombre"),
                    rs.getString("ciudad")
                ));
            }
        }
        return clientes;
    }
}
public Cliente findById(long id) throws SQLException {
    String sql = "SELECT id, nombre, ciudad FROM clientes WHERE id = ?";

    try (Connection conn = ConexionBD.getInstance();
         PreparedStatement ps = conn.prepareStatement(sql)) {

        ps.setLong(1, id);

        try (ResultSet rs = ps.executeQuery()) {
            if (rs.next()) {
                return new Cliente(
                    rs.getLong("id"),
                    rs.getString("nombre"),
                    rs.getString("ciudad")
                );
            }
        }
    }
    return null;
}
public int insert(Cliente cliente) throws SQLException {
    String sql = "INSERT INTO clientes(nombre, ciudad) VALUES (?, ?)";

    try (Connection conn = ConexionBD.getInstance();
         PreparedStatement ps = conn.prepareStatement(sql)) {

        ps.setString(1, cliente.getNombre());
        ps.setString(2, cliente.getCiudad());
        return ps.executeUpdate();
    }
}
public int updateCiudad(long id, String ciudad) throws SQLException {
    String sql = "UPDATE clientes SET ciudad = ? WHERE id = ?";

    try (Connection conn = ConexionBD.getInstance();
         PreparedStatement ps = conn.prepareStatement(sql)) {

        ps.setString(1, ciudad);
        ps.setLong(2, id);
        return ps.executeUpdate();
    }
}
public int delete(long id) throws SQLException {
    String sql = "DELETE FROM clientes WHERE id = ?";

    try (Connection conn = ConexionBD.getInstance();
         PreparedStatement ps = conn.prepareStatement(sql)) {

        ps.setLong(1, id);
        return ps.executeUpdate();
    }
}

El patrón DAO ayuda a que el sistema sea más mantenible, testeable y ordenado. La interfaz de usuario no debería saber cómo se ejecuta un SQL, igual que el usuario de una app de banca no necesita saber qué tabla guarda su saldo.

Warning

Singleton y DAO no son magia. Si los usas sin criterio, puedes acabar con dependencias difíciles de probar o con una única conexión compartida en un contexto donde harían falta más garantías.

Question

  1. ¿Por qué DAO mejora el mantenimiento del proyecto?
  2. ¿Qué ventaja aporta findById frente a repetir SQL en varios sitios?
  3. ¿En qué situaciones una sola conexión compartida puede ser mala idea?
  4. ¿Qué diferencia hay entre una clase de dominio y una clase DAO?

Gestión de recursos y transacciones

En JDBC, trabajar bien con la base de datos no consiste solo en consultar datos: también hay que cerrar recursos, controlar errores y asegurar que las operaciones críticas se guarden o se deshagan correctamente. Aquí es donde el código pasa de “funciona en mi máquina” a “resiste el mundo real”.

Try-with-resources

La forma moderna y más limpia de cerrar conexiones, sentencias y resultados es try-with-resources. Este bloque cierra automáticamente todo lo que implemente AutoCloseable, evitando fugas de memoria y conexiones abiertas que se quedan ahí como una pestaña olvidada del navegador desde 2021.

String sql = "SELECT id, nombre, ciudad FROM clientes";

try (Connection conn = DriverManager.getConnection(url, user, pass);
     PreparedStatement ps = conn.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {

    while (rs.next()) {
        System.out.println(
            rs.getLong("id") + " - " +
            rs.getString("nombre") + " - " +
            rs.getString("ciudad")
        );
    }
}
try (Connection conn = DriverManager.getConnection(url, user, pass);
     Statement st = conn.createStatement()) {

    st.executeUpdate("UPDATE clientes SET ciudad = 'Madrid' WHERE id = 5");
}
try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    conn.setAutoCommit(false);
    // operaciones
    conn.commit();
}

Tip

try-with-resources es la versión adulta de “cierro todo al salir”. En proyectos reales te ahorra errores, código repetido y más de un dolor de cabeza.

Manejo de excepciones

En acceso a BD lo normal es manejar, como mínimo, SQLException. Ese error puede esconder cosas muy distintas: usuario incorrecto, contraseña mal, host caído, SQL mal escrita, tabla inexistente o incluso timeout de conexión. Conviene tratar el error con mensajes útiles, no con un simple “falló”.

try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    System.out.println("Conexión correcta");
} catch (SQLException e) {
    System.err.println("Error al conectar con la base de datos: " + e.getMessage());
}
try (Connection conn = DriverManager.getConnection(url, user, pass);
     PreparedStatement ps = conn.prepareStatement(
         "SELECT id, nombre FROM clientes WHERE ciudad = ?"
     )) {

    ps.setString(1, "León");

    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getString("nombre"));
        }
    }
} catch (SQLException e) {
    e.printStackTrace();
}
catch (SQLException e) {
    if (e.getErrorCode() == 1049) {
        System.err.println("La base de datos no existe.");
    } else {
        System.err.println("Error SQL: " + e.getMessage());
    }
}

Warning

No muestres errores técnicos sin contexto al usuario final. Un mensaje útil en producción es mejor que un stack trace gigante que parece que la app ha invocado a un demonio de Java.

Commit y rollback

Cuando una operación necesita varias sentencias, debes pensar en términos de transacción. Si una falla, puedes revertir todo con rollback; si todo sale bien, confirmas con commit. Esto es esencial en operaciones como transferencias, reservas, pedidos o movimientos de stock.

try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    conn.setAutoCommit(false);

    try (PreparedStatement ps1 = conn.prepareStatement(
             "UPDATE cuentas SET saldo = saldo - ? WHERE id = ?"
         );
         PreparedStatement ps2 = conn.prepareStatement(
             "UPDATE cuentas SET saldo = saldo + ? WHERE id = ?"
         )) {

        ps1.setBigDecimal(1, new java.math.BigDecimal("250.00"));
        ps1.setInt(2, 1);
        ps1.executeUpdate();

        ps2.setBigDecimal(1, new java.math.BigDecimal("250.00"));
        ps2.setInt(2, 2);
        ps2.executeUpdate();

        conn.commit();
    } catch (SQLException e) {
        conn.rollback();
        throw e;
    }
}
try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    conn.setAutoCommit(false);

    try (PreparedStatement ps = conn.prepareStatement(
        "INSERT INTO pedidos(cliente_id, fecha, total) VALUES (?, ?, ?)"
    )) {
        ps.setInt(1, 12);
        ps.setDate(2, java.sql.Date.valueOf(java.time.LocalDate.now()));
        ps.setBigDecimal(3, new java.math.BigDecimal("89.90"));
        ps.executeUpdate();
    }

    conn.commit();
}
try (Connection conn = DriverManager.getConnection(url, user, pass)) {
    conn.setAutoCommit(false);
    // si algo falla
    conn.rollback();
}
Situación Acción
Todo correcto commit()
Fallo en una sentencia rollback()
Operación simple autoCommit activado
Operación crítica autoCommit(false)

Danger

Si desactivas autoCommit y no haces commit, puedes perder cambios aunque la consulta haya sido correcta. Es el clásico “yo sí lo guardé… bueno, creí que sí”.

Question

  1. ¿Por qué try-with-resources es mejor que cerrar recursos a mano?
  2. ¿En qué tipo de operación necesitas commit y rollback de forma obligatoria?
  3. ¿Qué puede pasar si desactivas autoCommit y olvidas confirmar?
  4. ¿Qué diferencia hay entre un error de conexión y un error SQL?

Ejemplos completos de DAO

Un DAO real no tiene solo un método. Suele agrupar operaciones de lectura, inserción, actualización y borrado para una entidad concreta. Eso permite que el resto de la aplicación trabaje con métodos claros y no con SQL disperso.

DAO de cliente

public class Cliente {
    private long id;
    private String nombre;
    private String ciudad;

    public Cliente(long id, String nombre, String ciudad) {
        this.id = id;
        this.nombre = nombre;
        this.ciudad = ciudad;
    }

    public long getId() { return id; }
    public String getNombre() { return nombre; }
    public String getCiudad() { return ciudad; }
}
public class ClienteDAO {
    public List<Cliente> findAll() throws SQLException {
        List<Cliente> clientes = new ArrayList<>();
        String sql = "SELECT id, nombre, ciudad FROM clientes ORDER BY nombre";

        try (Connection conn = ConexionBD.getInstance();
             Statement st = conn.createStatement();
             ResultSet rs = st.executeQuery(sql)) {

            while (rs.next()) {
                clientes.add(new Cliente(
                    rs.getLong("id"),
                    rs.getString("nombre"),
                    rs.getString("ciudad")
                ));
            }
        }
        return clientes;
    }
}
public Cliente findById(long id) throws SQLException {
    String sql = "SELECT id, nombre, ciudad FROM clientes WHERE id = ?";

    try (Connection conn = ConexionBD.getInstance();
         PreparedStatement ps = conn.prepareStatement(sql)) {

        ps.setLong(1, id);

        try (ResultSet rs = ps.executeQuery()) {
            if (rs.next()) {
                return new Cliente(
                    rs.getLong("id"),
                    rs.getString("nombre"),
                    rs.getString("ciudad")
                );
            }
        }
    }
    return null;
}
public int insert(Cliente cliente) throws SQLException {
    String sql = "INSERT INTO clientes(nombre, ciudad) VALUES (?, ?)";

    try (Connection conn = ConexionBD.getInstance();
         PreparedStatement ps = conn.prepareStatement(sql)) {

        ps.setString(1, cliente.getNombre());
        ps.setString(2, cliente.getCiudad());
        return ps.executeUpdate();
    }
}
public int update(Cliente cliente) throws SQLException {
    String sql = "UPDATE clientes SET nombre = ?, ciudad = ? WHERE id = ?";

    try (Connection conn = ConexionBD.getInstance();
         PreparedStatement ps = conn.prepareStatement(sql)) {

        ps.setString(1, cliente.getNombre());
        ps.setString(2, cliente.getCiudad());
        ps.setLong(3, cliente.getId());
        return ps.executeUpdate();
    }
}
public int delete(long id) throws SQLException {
    String sql = "DELETE FROM clientes WHERE id = ?";

    try (Connection conn = ConexionBD.getInstance();
         PreparedStatement ps = conn.prepareStatement(sql)) {

        ps.setLong(1, id);
        return ps.executeUpdate();
    }
}

Uso desde la capa de negocio

ClienteDAO dao = new ClienteDAO();

List<Cliente> lista = dao.findAll();
for (Cliente c : lista) {
    System.out.println(c.getNombre());
}
Cliente cliente = dao.findById(10L);
if (cliente != null) {
    System.out.println(cliente.getCiudad());
}
Cliente nuevo = new Cliente(0L, "Lucía", "León");
dao.insert(nuevo);

Tip

La capa de negocio debería hablar con el DAO, no con la BD directamente. Es como tener un jefe de equipo que coordina, en vez de mandar a cada persona a la cocina por su cuenta.

DAO con transacción

public void transferirSaldo(int origen, int destino, BigDecimal cantidad) throws SQLException {
    try (Connection conn = ConexionBD.getInstance()) {
        conn.setAutoCommit(false);

        try (PreparedStatement ps1 = conn.prepareStatement(
                 "UPDATE cuentas SET saldo = saldo - ? WHERE id = ?"
             );
             PreparedStatement ps2 = conn.prepareStatement(
                 "UPDATE cuentas SET saldo = saldo + ? WHERE id = ?"
             )) {

            ps1.setBigDecimal(1, cantidad);
            ps1.setInt(2, origen);
            ps1.executeUpdate();

            ps2.setBigDecimal(1, cantidad);
            ps2.setInt(2, destino);
            ps2.executeUpdate();

            conn.commit();
        } catch (SQLException e) {
            conn.rollback();
            throw e;
        }
    }
}
public int crearPedidoConLineas(Pedido pedido, List<LineaPedido> lineas) throws SQLException {
    try (Connection conn = ConexionBD.getInstance()) {
        conn.setAutoCommit(false);

        try {
            // insertar pedido
            // insertar líneas
            conn.commit();
            return 1;
        } catch (SQLException e) {
            conn.rollback();
            throw e;
        }
    }
}

Warning

Si una operación debe ser atómica, no la fragmentes en métodos que se ejecutan por separado sin control transaccional. La BD no perdona la improvisación.

Question

  1. ¿Qué métodos debería tener un DAO bien diseñado?
  2. ¿Por qué findById suele devolver null si no encuentra datos?
  3. ¿Qué ventaja tiene que el DAO use PreparedStatement?
  4. ¿Por qué una transferencia bancaria debe ir dentro de una transacción?

Buenas prácticas

En acceso a datos hay varias reglas que no son “bonitas recomendaciones”, sino casi supervivencia profesional. La primera es cerrar recursos siempre; la segunda, parametrizar consultas; la tercera, separar responsabilidades; la cuarta, no repetir SQL innecesariamente.

Reglas clave

// Usa try-with-resources
try (Connection conn = ...; PreparedStatement ps = ...; ResultSet rs = ...) {
    ...
}
// Parametriza
String sql = "SELECT * FROM clientes WHERE ciudad = ?";
// Separa acceso a datos de la lógica de negocio
ClienteDAO dao = new ClienteDAO();
// Maneja errores de forma clara
catch (SQLException e) {
    log.error("Error al consultar clientes", e);
}

Tip

Si el SQL cambia mucho, seguramente pertenece al DAO. Si la regla de negocio cambia mucho, seguramente pertenece al servicio. Si la interfaz cambia mucho, seguramente pertenece a la vista.

Errores típicos del alumnado

// Error típico: olvidar cerrar
Connection conn = DriverManager.getConnection(url, user, pass);
Statement st = conn.createStatement();
// Error típico: usar índices sin revisar
rs.getString(0); // Incorrecto
// Error típico: concatenar texto
String sql = "SELECT * FROM alumnos WHERE nombre = '" + nombre + "'";
// Error típico: mezclar SQL con interfaz
JOptionPane.showMessageDialog(null, rs.getString("nombre"));

Danger

No uses SELECT * por costumbre en código de producción. Trae solo las columnas que necesitas. Es más claro, más eficiente y evita sorpresas cuando la tabla cambia.

Question

  1. ¿Qué ventaja tiene pedir solo las columnas necesarias?
  2. ¿Por qué SELECT * puede ser mala idea en un proyecto real?
  3. ¿Qué problema hay en mezclar SQL con interfaz gráfica?
  4. ¿Qué error típico cometes si usas índices desde cero en ResultSet?

Aplicación real

En empresas, JDBC sigue siendo una base sólida para herramientas internas, servicios sencillos, integraciones y proyectos docentes que necesitan control total sobre SQL. Aunque muchos marcos modernos abstraen gran parte del acceso, entender JDBC es lo que permite depurar problemas y saber qué está pasando por debajo.

Un ejemplo muy típico es un panel de administración para productos, alumnos o pedidos. El backend Java recibe una petición, el DAO consulta la base de datos, el ResultSet se convierte en objetos y la respuesta vuelve a la capa web. Esa cadena es la misma idea que usarás en aplicaciones Spring, Jakarta EE o incluso microservicios.

Otro caso real es la migración entre motores. Si el acceso está bien encapsulado, cambiar el driver y ajustar la URL puede ser bastante más sencillo que tocar toda la aplicación. Eso es una de las grandes razones por las que JDBC sigue siendo tan importante en la formación y en la industria.

Para Saber Más

  • Documentación oficial de JDBC en Oracle: https://docs.oracle.com/javase/tutorial/jdbc/
  • Guía oficial de MySQL Connector/J: https://dev.mysql.com/doc/connector-j/en/
  • Notas de versión de MySQL Connector/J: https://dev.mysql.com/doc/relnotes/connector-j/en/
  • API java.sql en Java SE: https://docs.oracle.com/en/java/javase/26/docs/api/java.sql/java/sql/Driver.html




📚 ANEXO: Conexión JDBC con SQLite en IntelliJ IDEA


🎯 Introducción

Este anexo explica paso a paso cómo configurar SQLite con Java/JDBC en IntelliJ IDEA sin Maven, incluyendo la gestión visual con DB Browser for SQLite.

SQLite es ideal para:

  • ✅ Proyectos locales y portátiles
  • ✅ Prácticas y prototipos
  • ✅ Aplicaciones sin servidor
  • ✅ Desarrollo rápido

📋 PASOS PARA LA CONEXIÓN

PASO 1: Descargar Driver JDBC SQLite

  1. Ve a: https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc
  2. Última versión estable (ej: sqlite-jdbc-3.49.1.0.jar)
  3. DescargarGuardar en proyecto/lib/
    miProyecto/
    ├── lib/
    │   └── sqlite-jdbc-3.49.1.0.jar  ← ¡IMPORTANTE!
    ├── src/
    └── miBase.sqlite
    

PASO 2: Configurar IntelliJ IDEA

1. Clic derecho proyecto → Open Module Settings (F4)
2. Libraries → + → Java
3. Seleccionar sqlite-jdbc-*.jar
4. OK → Apply → OK

✅ Verificar: En Project Structure → Libraries debe aparecer el .jar

PASO 3: Estructura del código base

import java.sql.*;

public class ConexionSQLite {
    public static void main(String[] args) {
        String url = "jdbc:sqlite:miBase.sqlite";

        // Cargar driver explícitamente
        try {
            Class.forName("org.sqlite.JDBC");
        } catch (ClassNotFoundException e) {
            System.err.println("❌ Driver no encontrado");
            return;
        }

        // Conectar
        try (Connection conn = DriverManager.getConnection(url)) {
            System.out.println("✅ Conexión OK");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

⚠️ URL correcta: jdbc:sqlite:nombreArchivo.sqlite


🛠️ GESTIÓN CON DB BROWSER FOR SQLITE

Descarga e instalación

  • Web oficial: https://sqlitebrowser.org/
  • Windows/Mac/Linux: Descargar versión estable
  • Portable: Sin instalación (copiar y usar)

Crear base de datos desde cero

1. Abrir DB Browser
2. New Database → "alumnos.sqlite"
3. Execute SQL → Pegar esquema:

CREATE TABLE alumno (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nombre TEXT NOT NULL,
    apellido1 TEXT NOT NULL,
    apellido2 TEXT,
    ciudad TEXT NOT NULL,
    categoria INTEGER NOT NULL
);

4. Write Changes → OK

Insertar datos de prueba

INSERT INTO alumno VALUES 
(1,'Ana','García','López','León',1),
(2,'Luis','Pérez','Martín','León',2),
(3,'Marta','Sánchez','Ruiz','Madrid',1);

💾 Recordar: Ctrl+S para guardar cambios

Funcionalidades útiles

Acción Cómo
Ver datos pestaña Browse Data
Ejecutar SQL pestaña Execute SQL
Editar celdas doble clic en Browse Data
Exportar CSV File → Export → Table as CSV
Importar CSV File → Import → CSV file

🔧 ESTRUCTURA DEL PROYECTO FINAL

miProyecto/
├── lib/
│   └── sqlite-jdbc-3.49.1.0.jar     ← Driver JDBC
├── src/
│   └── MiClase.java                 ← Código Java
├── alumnos.sqlite                   ← Base de datos
└── README.md                        ← Documentación

🚀 PROGRAMA DE EJEMPLO COMPLETO

import java.sql.*;

public class EjemploCompleto {
    private static final String URL = "jdbc:sqlite:alumnos.sqlite";

    public static void main(String[] args) {
        // 1. Cargar driver
        cargarDriver();

        // 2. Crear tabla
        crearTabla();

        // 3. Insertar datos prueba
        insertarDatosPrueba();

        // 4. Consultar
        listarAlumnos();
    }

    private static void cargarDriver() {
        try {
            Class.forName("org.sqlite.JDBC");
            System.out.println("✅ Driver cargado");
        } catch (ClassNotFoundException e) {
            System.err.println("❌ Driver no encontrado");
            System.exit(1);
        }
    }

    private static void crearTabla() {
        String sql = """
            CREATE TABLE IF NOT EXISTS alumno (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                nombre TEXT NOT NULL,
                apellido1 TEXT NOT NULL,
                ciudad TEXT NOT NULL,
                categoria INTEGER NOT NULL
            )
            """;
        ejecutarSQL(sql, "Creando tabla");
    }

    private static void insertarDatosPrueba() {
        String sql = """
            INSERT OR IGNORE INTO alumno 
            (nombre, apellido1, ciudad, categoria) 
            VALUES 
            ('Ana', 'García', 'León', 1),
            ('Luis', 'Pérez', 'León', 2)
            """;
        ejecutarSQL(sql, "Datos prueba");
    }

    private static void listarAlumnos() {
        String sql = "SELECT * FROM alumno";
        try (Connection conn = DriverManager.getConnection(URL);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            System.out.println("📋 ALUMNOS:");
            while (rs.next()) {
                System.out.printf("ID:%d %s %s (%s, cat:%d)\n",
                    rs.getInt("id"),
                    rs.getString("nombre"),
                    rs.getString("apellido1"),
                    rs.getString("ciudad"),
                    rs.getInt("categoria"));
            }
        } catch (SQLException e) {
            System.err.println("Error consulta: " + e.getMessage());
        }
    }

    private static void ejecutarSQL(String sql, String mensaje) {
        try (Connection conn = DriverManager.getConnection(URL);
             Statement stmt = conn.createStatement()) {
            stmt.execute(sql);
            System.out.println("✅ " + mensaje);
        } catch (SQLException e) {
            System.err.println("❌ " + mensaje + ": " + e.getMessage());
        }
    }
}

❌ ERRORES COMUNES Y SOLUCIONES

Error Causa Solución
No suitable driver Falta JAR Añadir a Libraries
no such table Tabla no existe Crear con DB Browser
database is locked Archivo abierto Cerrar otras apps
ClassNotFoundException Driver no cargado Class.forName("org.sqlite.JDBC")

📱 WORKFLOW RECOMENDADO

1. Crear alumnos.sqlite (DB Browser)
2. Configurar JAR en IntelliJ
3. Código Java con Class.forName()
4. Probar conexión básica
5. Desarrollar CRUD
6. Verificar datos en DB Browser

💡 Consejo: Mantén DB Browser abierto para ver cambios en tiempo real mientras programas.