Cómo Conectar Java con MySQL (JDBC) — Tutorial Paso a Paso 2026
Conexión Java a MySQL con JDBC explicada línea por línea. Incluye CRUD completo, PreparedStatement, connection pool y los errores que te van a saltar.
Tabla de contenidos
Este es el tutorial que me habría ahorrado 4 horas de errores incomprensibles cuando estaba en DAM. Vamos a conectar Java con MySQL usando JDBC, paso a paso, con cada línea de código explicada.
No te voy a soltar la teoría de libro. Vamos directo al código que funciona, con los errores que te van a saltar y cómo solucionarlos.
TL;DR — Lo que vas a aprender:
- Configurar el proyecto con Maven y el conector MySQL
- Crear la conexión JDBC correctamente (sin fugas de memoria)
- CRUD completo con PreparedStatement (seguro contra SQL injection)
- Connection pool con HikariCP (lo que usan las empresas)
- Los 5 errores que te van a saltar y cómo arreglarlos
Paso 0: Lo que necesitas instalado
Antes de tocar código, verifica que tienes esto:
| Herramienta | Versión mínima | Comprobar con |
|---|---|---|
| Java JDK | 17+ | java --version |
| Maven | 3.9+ | mvn --version |
| MySQL Server | 8.0+ | mysql --version |
Si usas IntelliJ IDEA o Eclipse, Maven ya viene integrado.
Paso 1: Crear el proyecto Maven
Abre la terminal y crea un proyecto nuevo:
# Crear proyecto Maven con el archetype quickstart
mvn archetype:generate \
-DgroupId=com.ejemplo \
-DartifactId=java-mysql-demo \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.5 \
-DinteractiveMode=false
cd java-mysql-demo
Añadir el conector MySQL
Abre pom.xml y añade la dependencia dentro de <dependencies>:
<!-- pom.xml — añadir dentro de <dependencies> -->
<dependency>
<!-- Este es el conector oficial de MySQL para Java -->
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.2.0</version>
</dependency>
Luego ejecuta mvn install para descargar la dependencia.
Paso 2: Crear la base de datos
Entra en MySQL y crea la base de datos de ejemplo:
-- Crear la base de datos
CREATE DATABASE IF NOT EXISTS tienda_daw;
USE tienda_daw;
-- Crear tabla de productos
CREATE TABLE productos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(200) NOT NULL,
precio DECIMAL(10,2) NOT NULL CHECK (precio >= 0),
cantidad INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insertar datos de ejemplo para probar
INSERT INTO productos (nombre, precio, cantidad) VALUES
('Teclado mecánico', 79.99, 15),
('Monitor 27"', 299.00, 8),
('Ratón gaming', 49.50, 25);
Paso 3: La conexión JDBC (bien hecha)
Aquí es donde el 80% de los estudiantes meten la pata. Vamos a hacerlo bien desde el principio:
// ConexionDB.java — Clase que gestiona la conexión a MySQL
// IMPORTANTE: nunca hardcodees las credenciales en producción
package com.ejemplo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConexionDB {
// Datos de conexión (en producción, usa variables de entorno)
private static final String URL = "jdbc:mysql://localhost:3306/tienda_daw";
private static final String USER = "root";
private static final String PASSWORD = "tu_password_aqui";
/**
* Obtiene una conexión a la base de datos MySQL.
*
* Parámetros de la URL JDBC:
* - localhost:3306 → host y puerto de MySQL
* - tienda_daw → nombre de la base de datos
* - useSSL=false → no usar SSL en desarrollo local
* - serverTimezone=UTC → evita errores de zona horaria
*/
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(
URL + "?useSSL=false&serverTimezone=UTC",
USER,
PASSWORD
);
}
/**
* Comprueba que la conexión funciona.
* Ejecuta esto primero para verificar que todo está bien.
*/
public static void main(String[] args) {
// try-with-resources: cierra la conexión automáticamente
try (Connection conn = getConnection()) {
if (conn != null && !conn.isClosed()) {
System.out.println("✅ Conexión exitosa a MySQL!");
System.out.println(" Base de datos: " + conn.getCatalog());
}
} catch (SQLException e) {
System.err.println("❌ Error de conexión: " + e.getMessage());
// Los códigos de error más comunes:
// 08001 → MySQL no está corriendo
// 28000 → usuario/contraseña incorrectos
// 42000 → la base de datos no existe
System.err.println(" Código SQL: " + e.getSQLState());
}
}
}
Ejecuta esta clase primero. Si ves ✅ Conexión exitosa, puedes seguir. Si no, revisa la sección de errores al final.
Paso 4: CRUD completo con PreparedStatement
Ahora sí — las 4 operaciones que necesitas para cualquier proyecto:
// ProductoDAO.java — Data Access Object para la tabla productos
// DAO es el patrón que separa la lógica de BBDD del resto de la app
package com.ejemplo;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class ProductoDAO {
// ─── CREATE ───────────────────────────────────────────────
// Inserta un producto nuevo y devuelve el ID generado
public int insertar(String nombre, double precio, int cantidad)
throws SQLException {
// ⚠️ SIEMPRE usar ? en vez de concatenar strings
// Esto previene SQL Injection (seguridad obligatoria)
String sql = "INSERT INTO productos (nombre, precio, cantidad) "
+ "VALUES (?, ?, ?)";
try (Connection conn = ConexionDB.getConnection();
// RETURN_GENERATED_KEYS → para obtener el ID auto-generado
PreparedStatement ps = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS)) {
// Asignar valores a cada ? (índice empieza en 1, no en 0)
ps.setString(1, nombre); // ? número 1 = nombre
ps.setDouble(2, precio); // ? número 2 = precio
ps.setInt(3, cantidad); // ? número 3 = cantidad
// executeUpdate() para INSERT, UPDATE, DELETE
// executeQuery() es para SELECT
int filas = ps.executeUpdate();
// Obtener el ID que MySQL generó automáticamente
if (filas > 0) {
ResultSet rs = ps.getGeneratedKeys();
if (rs.next()) {
return rs.getInt(1); // El ID generado
}
}
return -1;
}
// La conexión se cierra automáticamente gracias al try-with-resources
}
// ─── READ (todos) ─────────────────────────────────────────
public List<String> listarTodos() throws SQLException {
String sql = "SELECT id, nombre, precio, cantidad FROM productos "
+ "ORDER BY nombre";
List<String> productos = new ArrayList<>();
try (Connection conn = ConexionDB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
// Recorrer cada fila del resultado
while (rs.next()) {
// Acceder a las columnas por nombre (más legible que por índice)
String linea = String.format(
"[%d] %s — %.2f€ (stock: %d)",
rs.getInt("id"),
rs.getString("nombre"),
rs.getDouble("precio"),
rs.getInt("cantidad")
);
productos.add(linea);
}
}
return productos;
}
// ─── READ (uno por ID) ────────────────────────────────────
public String buscarPorId(int id) throws SQLException {
String sql = "SELECT * FROM productos WHERE id = ?";
try (Connection conn = ConexionDB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return String.format(
"%s — %.2f€ (stock: %d)",
rs.getString("nombre"),
rs.getDouble("precio"),
rs.getInt("cantidad")
);
}
return null; // No encontrado
}
}
}
// ─── UPDATE ───────────────────────────────────────────────
public boolean actualizar(int id, String nombre, double precio,
int cantidad) throws SQLException {
String sql = "UPDATE productos SET nombre = ?, precio = ?, "
+ "cantidad = ? WHERE id = ?";
try (Connection conn = ConexionDB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, nombre);
ps.setDouble(2, precio);
ps.setInt(3, cantidad);
ps.setInt(4, id); // El WHERE va al final
// executeUpdate() devuelve el número de filas afectadas
return ps.executeUpdate() > 0;
}
}
// ─── DELETE ───────────────────────────────────────────────
public boolean eliminar(int id) throws SQLException {
String sql = "DELETE FROM productos WHERE id = ?";
try (Connection conn = ConexionDB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
return ps.executeUpdate() > 0;
}
}
}
Probarlo todo junto
// Main.java — Programa principal para probar el CRUD
package com.ejemplo;
public class Main {
public static void main(String[] args) {
ProductoDAO dao = new ProductoDAO();
try {
// 1. Insertar
int id = dao.insertar("Webcam 4K", 89.99, 12);
System.out.println("✅ Producto creado con ID: " + id);
// 2. Listar todos
System.out.println("\n📋 Todos los productos:");
dao.listarTodos().forEach(System.out::println);
// 3. Buscar uno
System.out.println("\n🔍 Producto ID " + id + ": "
+ dao.buscarPorId(id));
// 4. Actualizar
dao.actualizar(id, "Webcam 4K Pro", 119.99, 10);
System.out.println("\n✏️ Actualizado: " + dao.buscarPorId(id));
// 5. Eliminar
dao.eliminar(id);
System.out.println("\n🗑️ Eliminado. Existe? "
+ (dao.buscarPorId(id) == null ? "No" : "Sí"));
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
}
}
}
Paso 5: Connection Pool con HikariCP (nivel pro)
En un proyecto real, nunca abres una conexión nueva para cada consulta. Usas un connection pool que reutiliza conexiones. HikariCP es el estándar en el mundo Java.
Añade la dependencia en pom.xml:
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>6.2.1</version>
</dependency>
Y reemplaza la clase ConexionDB:
// ConexionDB.java — Versión con connection pool (producción)
// HikariCP es el pool más rápido y usado en Java empresarial
package com.ejemplo;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class ConexionDB {
private static final HikariDataSource ds;
// Bloque estático: se ejecuta UNA sola vez al cargar la clase
static {
HikariConfig config = new HikariConfig();
// URL de conexión (igual que antes)
config.setJdbcUrl("jdbc:mysql://localhost:3306/tienda_daw"
+ "?useSSL=false&serverTimezone=UTC");
config.setUsername("root");
config.setPassword("tu_password_aqui");
// Configuración del pool
config.setMaximumPoolSize(10); // Máx 10 conexiones simultáneas
config.setMinimumIdle(2); // Mínimo 2 conexiones listas
config.setIdleTimeout(300000); // 5 min sin uso → cerrar
config.setConnectionTimeout(10000); // 10s máx esperando conexión
ds = new HikariDataSource(config);
}
public static Connection getConnection() throws SQLException {
return ds.getConnection(); // Devuelve una conexión del pool
}
// Cerrar el pool al apagar la app (importante en servidores)
public static void close() {
if (ds != null && !ds.isClosed()) {
ds.close();
}
}
}
El ProductoDAO no cambia nada — sigue usando ConexionDB.getConnection(). Esa es la gracia del patrón DAO.
Errores típicos que te darán el 0 en el examen
1. Communications link failure
Causa: MySQL no está corriendo o la URL está mal.
# Comprobar si MySQL está activo
# Linux/Mac:
sudo systemctl status mysql
# Windows:
net start MySQL80
2. Access denied for user 'root'@'localhost'
Causa: Contraseña incorrecta.
-- Cambiar la contraseña de root en MySQL
ALTER USER 'root'@'localhost' IDENTIFIED BY 'nueva_password';
FLUSH PRIVILEGES;
3. No suitable driver found
Causa: Falta el conector MySQL en las dependencias.
# Verificar que Maven descargó la dependencia
mvn dependency:tree | grep mysql
# Debería mostrar: com.mysql:mysql-connector-j:9.2.0
4. SQL Injection (esto es suspense directo)
// ❌ NUNCA hagas esto (vulnerable a SQL Injection)
String sql = "SELECT * FROM productos WHERE nombre = '" + nombre + "'";
// Si alguien pone: ' OR 1=1; DROP TABLE productos; --
// Se ejecuta la sentencia maliciosa
// ✅ SIEMPRE usa PreparedStatement
String sql = "SELECT * FROM productos WHERE nombre = ?";
ps.setString(1, nombre); // Escapa automáticamente los caracteres peligrosos
5. Fugas de conexiones (Connection Leak)
// ❌ Si falla algo antes del close(), la conexión se queda abierta
Connection conn = ConexionDB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
// ... si aquí lanza una excepción, nunca se cierra
conn.close();
// ✅ try-with-resources cierra TODO automáticamente, incluso si falla
try (Connection conn = ConexionDB.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
// Si falla aquí, conn, ps y rs se cierran automáticamente
}
Resumen rápido
| Concepto | Qué usar | Por qué |
|---|---|---|
| Conexión | DriverManager o HikariCP | DriverManager para aprender, HikariCP para producción |
| Consultas | PreparedStatement | Previene SQL injection, obligatorio |
| Recursos | try-with-resources | Cierra conexiones automáticamente |
| Patrón | DAO | Separa lógica de BBDD del resto |
Si te has atascado con la conexión a base de datos o tu proyecto de DAW/DAM, prueba el generador de prompts para debugging — pega el error y te crea el prompt perfecto para que una IA te lo solucione.
¿Buscas ideas para tu proyecto final? Aquí tienes 10 ideas de TFG para DAW y DAM con stack y tiempo estimado.