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
- ¿Por qué una base de datos relacional no puede almacenar un objeto Java tal cual?
- ¿Qué ventajas tiene separar el mapeo de datos en una clase DAO?
- ¿Qué problemas aparecen cuando la lógica SQL se dispersa por toda la aplicación?
- ¿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
- ¿Qué aporta JDBC frente a una solución dependiente del sistema operativo?
- ¿Qué es exactamente lo que cambia si migras de MySQL a PostgreSQL?
- ¿Por qué los drivers tipo 4 son tan usados hoy?
- ¿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
- ¿Qué papel juega
DriverManageren el arranque de una conexión? - ¿Qué objeto se encarga de ejecutar realmente la sentencia SQL?
- ¿Por qué
ResultSetno es una lista normal de Java? - ¿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 | Sí |
| 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.
Navegación de ResultSet
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
- ¿Qué diferencia real hay entre
executeQueryyexecuteUpdate? - ¿Por qué
PreparedStatementes mejor para datos introducidos por el usuario? - ¿Cuándo te interesa un
ResultSetdesplazable? - ¿Por qué usar índices en
ResultSetpuede 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
- ¿Qué diferencia hay entre DDL y DML?
- ¿Por qué las transacciones son tan importantes en banca, stock o pedidos?
- ¿Qué ocurre si una operación falla dentro de una transacción?
- ¿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
- ¿Qué diferencia hay entre
DatabaseMetaDatayResultSetMetaData? - ¿Para qué sirve
getTables? - ¿Qué utilidad tiene
getColumnCount? - ¿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
- ¿Por qué DAO mejora el mantenimiento del proyecto?
- ¿Qué ventaja aporta
findByIdfrente a repetir SQL en varios sitios? - ¿En qué situaciones una sola conexión compartida puede ser mala idea?
- ¿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
- ¿Por qué
try-with-resourceses mejor que cerrar recursos a mano? - ¿En qué tipo de operación necesitas
commityrollbackde forma obligatoria? - ¿Qué puede pasar si desactivas
autoCommity olvidas confirmar? - ¿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
- ¿Qué métodos debería tener un DAO bien diseñado?
- ¿Por qué
findByIdsuele devolvernullsi no encuentra datos? - ¿Qué ventaja tiene que el DAO use
PreparedStatement? - ¿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
- ¿Qué ventaja tiene pedir solo las columnas necesarias?
- ¿Por qué
SELECT *puede ser mala idea en un proyecto real? - ¿Qué problema hay en mezclar SQL con interfaz gráfica?
- ¿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.sqlen 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
- Ve a: https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc
- Última versión estable (ej:
sqlite-jdbc-3.49.1.0.jar) - Descargar → Guardar 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.