Casi cualquier aplicación real necesita guardar datos de forma persistente: usuarios, productos, pedidos, historial de transacciones. Para eso existen las bases de datos, y MySQL es una de las más usadas en el mundo. Conectar Java con MySQL es una habilidad fundamental para cualquier desarrollador backend o fullstack.
En este artículo aprenderás paso a paso cómo establecer esa conexión usando JDBC (Java Database Connectivity), la API estándar de Java para comunicarse con bases de datos relacionales. Verás cómo ejecutar consultas, insertar registros, actualizar datos, eliminar filas y manejar correctamente los recursos y errores.
Qué es JDBC
JDBC es una API de Java que define un conjunto de interfaces y clases para conectarse a bases de datos relacionales, ejecutar sentencias SQL y procesar los resultados. No es específica de MySQL: el mismo código JDBC funciona con PostgreSQL, Oracle, SQLite o cualquier otra base de datos que tenga un driver compatible.
El driver es la pieza clave: es una biblioteca que implementa las interfaces de JDBC para una base de datos concreta. Para MySQL, el driver oficial se llama MySQL Connector/J.
El flujo de trabajo con JDBC es siempre el mismo:
- Cargar el driver de la base de datos.
- Establecer una conexión con la base de datos.
- Crear una sentencia SQL.
- Ejecutar la sentencia.
- Procesar los resultados (si los hay).
- Cerrar los recursos.
Requisitos previos
Antes de escribir código, necesitas tener instalado y configurado lo siguiente:
- Java JDK: versión 8 o superior.
- MySQL Server: corriendo localmente o en un servidor remoto.
- MySQL Connector/J: el driver JDBC de MySQL.
- Un IDE o editor: IntelliJ IDEA, Eclipse o VS Code.
Si no tienes MySQL instalado, puedes descargarlo desde el sitio oficial de MySQL o usar XAMPP, que incluye MySQL junto con Apache y PHP en una sola instalación.
Paso 1: Agregar el driver MySQL Connector/J
Tienes dos formas de agregar el driver a tu proyecto.
Con Maven (recomendado)
Si tu proyecto usa Maven, agrega esta dependencia en tu archivo pom.xml:
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>Con Gradle
implementation 'com.mysql:mysql-connector-j:8.3.0'Sin gestor de dependencias
Descarga el archivo .jar desde el sitio oficial de MySQL (dev.mysql.com/downloads/connector/j/) y agrégalo manualmente al classpath de tu proyecto. En IntelliJ IDEA: File → Project Structure → Libraries → el ícono + → Java → selecciona el .jar descargado.
Paso 2: Crear la base de datos y la tabla en MySQL
Antes de conectar, necesitas una base de datos con la que trabajar. Abre tu cliente MySQL (MySQL Workbench, DBeaver, o la terminal) y ejecuta:
-- Crear la base de datos
CREATE DATABASE tienda_java;
-- Seleccionar la base de datos
USE tienda_java;
-- Crear la tabla de productos
CREATE TABLE productos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
precio DECIMAL(10, 2) NOT NULL,
stock INT DEFAULT 0,
categoria VARCHAR(50)
);
-- Insertar algunos datos de prueba
INSERT INTO productos (nombre, precio, stock, categoria) VALUES
('Laptop Pro 15', 1299.99, 25, 'Electrónica'),
('Teclado Mecánico', 89.99, 150, 'Periféricos'),
('Monitor 27"', 349.99, 40, 'Electrónica'),
('Mouse Inalámbrico', 45.50, 200, 'Periféricos'),
('Auriculares Bluetooth', 129.99, 75, 'Audio');Paso 3: Establecer la primera conexión
La clase central de JDBC es DriverManager, que se encarga de establecer la conexión usando una URL de conexión, un usuario y una contraseña.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConexionMySQL {
// Datos de conexión
private static final String URL = "jdbc:mysql://localhost:3306/tienda_java";
private static final String USUARIO = "root";
private static final String PASSWORD = "tu_contraseña";
public static void main(String[] args) {
try (Connection conexion = DriverManager.getConnection(URL, USUARIO, PASSWORD)) {
System.out.println("¡Conexión exitosa a MySQL!");
System.out.println("Base de datos: " + conexion.getCatalog());
System.out.println("Driver: " + conexion.getMetaData().getDriverName());
} catch (SQLException e) {
System.out.println("Error al conectar: " + e.getMessage());
System.out.println("Código de error SQL: " + e.getErrorCode());
}
}
}La URL de conexión tiene el formato:
jdbc:mysql://[host]:[puerto]/[nombre_base_datos]jdbc:mysql://indica el protocolo y el tipo de base de datos.localhost:3306es el host y el puerto. El puerto 3306 es el predeterminado de MySQL.tienda_javaes el nombre de la base de datos.
Nota el uso de try-with-resources: la conexión se cierra automáticamente al salir del bloque, sin necesidad de llamar a conexion.close() manualmente.
Paso 4: Crear una clase de utilidad para la conexión
En lugar de repetir los datos de conexión en cada clase, lo mejor es centralizar la lógica en una clase utilitaria. En proyectos reales los datos sensibles se guardan en variables de entorno o archivos de configuración, nunca en el código fuente.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseConnection {
private static final String URL = "jdbc:mysql://localhost:3306/tienda_java"
+ "?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
private static final String USUARIO = "root";
private static final String PASSWORD = "tu_contraseña";
// Evita instanciar la clase
private DatabaseConnection() {}
public static Connection obtenerConexion() throws SQLException {
return DriverManager.getConnection(URL, USUARIO, PASSWORD);
}
}Los parámetros adicionales en la URL son importantes:
useSSL=false: desactiva SSL para conexiones locales de desarrollo.serverTimezone=UTC: evita problemas con zonas horarias.allowPublicKeyRetrieval=true: necesario en algunas versiones de MySQL 8.
Paso 5: Consultar datos con SELECT
Para ejecutar consultas que devuelven datos usas Statement o PreparedStatement y procesas los resultados con un ResultSet.
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class ConsultarProductos {
public static void main(String[] args) {
String sql = "SELECT id, nombre, precio, stock, categoria FROM productos";
try (
Connection conexion = DatabaseConnection.obtenerConexion();
Statement sentencia = conexion.createStatement();
ResultSet resultado = sentencia.executeQuery(sql)
) {
System.out.printf("%-5s %-25s %-10s %-8s %-15s%n",
"ID", "NOMBRE", "PRECIO", "STOCK", "CATEGORÍA");
System.out.println("-".repeat(65));
while (resultado.next()) {
int id = resultado.getInt("id");
String nombre = resultado.getString("nombre");
double precio = resultado.getDouble("precio");
int stock = resultado.getInt("stock");
String categoria = resultado.getString("categoria");
System.out.printf("%-5d %-25s $%-9.2f %-8d %-15s%n",
id, nombre, precio, stock, categoria);
}
} catch (SQLException e) {
System.out.println("Error en la consulta: " + e.getMessage());
}
}
}El ResultSet funciona como un cursor que empieza antes del primer resultado. Cada llamada a resultado.next() avanza al siguiente registro y devuelve false cuando no hay más. Para leer los valores usas métodos como getString(), getInt(), getDouble(), etc., pasando el nombre de la columna o su posición.
Paso 6: PreparedStatement para consultas seguras
Statement funciona para consultas fijas, pero nunca debes usarlo para construir SQL concatenando valores del usuario. Eso abre la puerta a inyección SQL, una de las vulnerabilidades más graves y comunes en aplicaciones web.
// ❌ NUNCA hagas esto: vulnerable a inyección SQL
String nombre = request.getParameter("nombre"); // dato del usuario
String sql = "SELECT * FROM productos WHERE nombre = '" + nombre + "'";
// Un atacante podría enviar: ' OR '1'='1 y obtener todos los registrosLa solución es PreparedStatement: precompila la consulta con marcadores de posición (?) y luego sustituye los valores de forma segura.
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class BuscarProductos {
public static void buscarPorCategoria(String categoria) {
String sql = "SELECT id, nombre, precio FROM productos WHERE categoria = ?";
try (
Connection conexion = DatabaseConnection.obtenerConexion();
PreparedStatement sentencia = conexion.prepareStatement(sql)
) {
// Sustituir el marcador ? con el valor real (índices empiezan en 1)
sentencia.setString(1, categoria);
try (ResultSet resultado = sentencia.executeQuery()) {
System.out.println("Productos en categoría: " + categoria);
System.out.println("-".repeat(40));
boolean hayResultados = false;
while (resultado.next()) {
hayResultados = true;
System.out.printf("ID: %d | %s | $%.2f%n",
resultado.getInt("id"),
resultado.getString("nombre"),
resultado.getDouble("precio"));
}
if (!hayResultados) {
System.out.println("No se encontraron productos.");
}
}
} catch (SQLException e) {
System.out.println("Error: " + e.getMessage());
}
}
public static void main(String[] args) {
buscarPorCategoria("Electrónica");
System.out.println();
buscarPorCategoria("Periféricos");
}
}Paso 7: Insertar registros con INSERT
Para sentencias que modifican datos (INSERT, UPDATE, DELETE) usas executeUpdate() en lugar de executeQuery(). Este método devuelve el número de filas afectadas.
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class InsertarProducto {
public static int insertar(String nombre, double precio, int stock, String categoria) {
String sql = "INSERT INTO productos (nombre, precio, stock, categoria) VALUES (?, ?, ?, ?)";
try (
Connection conexion = DatabaseConnection.obtenerConexion();
PreparedStatement sentencia = conexion.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS) // para obtener el ID generado
) {
sentencia.setString(1, nombre);
sentencia.setDouble(2, precio);
sentencia.setInt(3, stock);
sentencia.setString(4, categoria);
int filasAfectadas = sentencia.executeUpdate();
if (filasAfectadas > 0) {
// Obtener el ID generado automáticamente por AUTO_INCREMENT
try (ResultSet claves = sentencia.getGeneratedKeys()) {
if (claves.next()) {
int idGenerado = claves.getInt(1);
System.out.println("Producto insertado con ID: " + idGenerado);
return idGenerado;
}
}
}
} catch (SQLException e) {
System.out.println("Error al insertar: " + e.getMessage());
}
return -1;
}
public static void main(String[] args) {
insertar("Webcam HD", 79.99, 60, "Periféricos");
insertar("Disco SSD 1TB", 119.99, 90, "Almacenamiento");
}
}Paso 8: Actualizar registros con UPDATE
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class ActualizarProducto {
public static boolean actualizarPrecio(int id, double nuevoPrecio) {
String sql = "UPDATE productos SET precio = ? WHERE id = ?";
try (
Connection conexion = DatabaseConnection.obtenerConexion();
PreparedStatement sentencia = conexion.prepareStatement(sql)
) {
sentencia.setDouble(1, nuevoPrecio);
sentencia.setInt(2, id);
int filasAfectadas = sentencia.executeUpdate();
if (filasAfectadas > 0) {
System.out.println("Precio actualizado correctamente. ID: " + id);
return true;
} else {
System.out.println("No se encontró ningún producto con ID: " + id);
return false;
}
} catch (SQLException e) {
System.out.println("Error al actualizar: " + e.getMessage());
return false;
}
}
public static boolean actualizarStock(int id, int nuevoStock) {
String sql = "UPDATE productos SET stock = ? WHERE id = ?";
try (
Connection conexion = DatabaseConnection.obtenerConexion();
PreparedStatement sentencia = conexion.prepareStatement(sql)
) {
sentencia.setInt(1, nuevoStock);
sentencia.setInt(2, id);
int filasAfectadas = sentencia.executeUpdate();
return filasAfectadas > 0;
} catch (SQLException e) {
System.out.println("Error al actualizar stock: " + e.getMessage());
return false;
}
}
public static void main(String[] args) {
actualizarPrecio(1, 1199.99); // Laptop Pro 15 baja de precio
actualizarStock(2, 130); // Reducir stock del teclado
}
}Paso 9: Eliminar registros con DELETE
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class EliminarProducto {
public static boolean eliminar(int id) {
String sql = "DELETE FROM productos WHERE id = ?";
try (
Connection conexion = DatabaseConnection.obtenerConexion();
PreparedStatement sentencia = conexion.prepareStatement(sql)
) {
sentencia.setInt(1, id);
int filasAfectadas = sentencia.executeUpdate();
if (filasAfectadas > 0) {
System.out.println("Producto eliminado. ID: " + id);
return true;
} else {
System.out.println("No existe ningún producto con ID: " + id);
return false;
}
} catch (SQLException e) {
System.out.println("Error al eliminar: " + e.getMessage());
return false;
}
}
public static void main(String[] args) {
eliminar(6); // Eliminar la Webcam HD que insertamos antes
eliminar(99); // ID que no existe
}
}Paso 10: Transacciones
Una transacción agrupa varias operaciones en una unidad atómica: o todas se ejecutan con éxito, o ninguna se aplica. Es fundamental cuando realizas operaciones relacionadas que deben mantenerse consistentes, como transferir dinero entre dos cuentas o procesar un pedido que descuenta stock y crea un registro de venta.
Por defecto, JDBC trabaja en modo autocommit: cada sentencia se confirma automáticamente. Para usar transacciones debes desactivar ese modo.
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class TransaccionEjemplo {
public static void procesarVenta(int idProducto, int cantidadVendida, double precioTotal) {
String sqlDescontarStock = "UPDATE productos SET stock = stock - ? WHERE id = ? AND stock >= ?";
String sqlRegistrarVenta = "INSERT INTO ventas (id_producto, cantidad, total) VALUES (?, ?, ?)";
Connection conexion = null;
try {
conexion = DatabaseConnection.obtenerConexion();
// Desactivar autocommit para controlar la transacción manualmente
conexion.setAutoCommit(false);
// Operación 1: descontar stock
try (PreparedStatement stmtStock = conexion.prepareStatement(sqlDescontarStock)) {
stmtStock.setInt(1, cantidadVendida);
stmtStock.setInt(2, idProducto);
stmtStock.setInt(3, cantidadVendida);
int filasAfectadas = stmtStock.executeUpdate();
if (filasAfectadas == 0) {
throw new SQLException("Stock insuficiente para el producto ID: " + idProducto);
}
}
// Operación 2: registrar la venta
try (PreparedStatement stmtVenta = conexion.prepareStatement(sqlRegistrarVenta)) {
stmtVenta.setInt(1, idProducto);
stmtVenta.setInt(2, cantidadVendida);
stmtVenta.setDouble(3, precioTotal);
stmtVenta.executeUpdate();
}
// Si todo salió bien, confirmar la transacción
conexion.commit();
System.out.println("Venta procesada correctamente.");
} catch (SQLException e) {
// Si algo falló, revertir todos los cambios
System.out.println("Error en la transacción: " + e.getMessage());
if (conexion != null) {
try {
conexion.rollback();
System.out.println("Transacción revertida.");
} catch (SQLException ex) {
System.out.println("Error al hacer rollback: " + ex.getMessage());
}
}
} finally {
if (conexion != null) {
try {
conexion.setAutoCommit(true); // restaurar el modo autocommit
conexion.close();
} catch (SQLException e) {
System.out.println("Error al cerrar la conexión: " + e.getMessage());
}
}
}
}
}Paso 11: Estructura completa con patrón DAO
En proyectos reales, el código de acceso a datos no va mezclado con la lógica de negocio ni con la interfaz de usuario. El patrón DAO (Data Access Object) organiza todo el código de base de datos en clases dedicadas, una por entidad.
// Clase que representa un producto (modelo)
public class Producto {
private int id;
private String nombre;
private double precio;
private int stock;
private String categoria;
public Producto(int id, String nombre, double precio, int stock, String categoria) {
this.id = id;
this.nombre = nombre;
this.precio = precio;
this.stock = stock;
this.categoria = categoria;
}
// Getters
public int getId() { return id; }
public String getNombre() { return nombre; }
public double getPrecio() { return precio; }
public int getStock() { return stock; }
public String getCategoria() { return categoria; }
@Override
public String toString() {
return String.format("Producto{id=%d, nombre='%s', precio=%.2f, stock=%d, categoria='%s'}",
id, nombre, precio, stock, categoria);
}
}import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
// Clase DAO: centraliza todo el acceso a datos de Producto
public class ProductoDAO {
// Listar todos los productos
public List<Producto> listarTodos() {
List<Producto> productos = new ArrayList<>();
String sql = "SELECT id, nombre, precio, stock, categoria FROM productos";
try (
Connection con = DatabaseConnection.obtenerConexion();
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(sql)
) {
while (rs.next()) {
productos.add(mapearResultado(rs));
}
} catch (SQLException e) {
System.out.println("Error al listar productos: " + e.getMessage());
}
return productos;
}
// Buscar por ID
public Optional<Producto> buscarPorId(int id) {
String sql = "SELECT id, nombre, precio, stock, categoria FROM productos WHERE id = ?";
try (
Connection con = DatabaseConnection.obtenerConexion();
PreparedStatement ps = con.prepareStatement(sql)
) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return Optional.of(mapearResultado(rs));
}
}
} catch (SQLException e) {
System.out.println("Error al buscar producto: " + e.getMessage());
}
return Optional.empty();
}
// Insertar un producto
public int insertar(Producto producto) {
String sql = "INSERT INTO productos (nombre, precio, stock, categoria) VALUES (?, ?, ?, ?)";
try (
Connection con = DatabaseConnection.obtenerConexion();
PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)
) {
ps.setString(1, producto.getNombre());
ps.setDouble(2, producto.getPrecio());
ps.setInt(3, producto.getStock());
ps.setString(4, producto.getCategoria());
ps.executeUpdate();
try (ResultSet claves = ps.getGeneratedKeys()) {
if (claves.next()) return claves.getInt(1);
}
} catch (SQLException e) {
System.out.println("Error al insertar producto: " + e.getMessage());
}
return -1;
}
// Actualizar un producto
public boolean actualizar(Producto producto) {
String sql = "UPDATE productos SET nombre=?, precio=?, stock=?, categoria=? WHERE id=?";
try (
Connection con = DatabaseConnection.obtenerConexion();
PreparedStatement ps = con.prepareStatement(sql)
) {
ps.setString(1, producto.getNombre());
ps.setDouble(2, producto.getPrecio());
ps.setInt(3, producto.getStock());
ps.setString(4, producto.getCategoria());
ps.setInt(5, producto.getId());
return ps.executeUpdate() > 0;
} catch (SQLException e) {
System.out.println("Error al actualizar producto: " + e.getMessage());
return false;
}
}
// Eliminar un producto
public boolean eliminar(int id) {
String sql = "DELETE FROM productos WHERE id = ?";
try (
Connection con = DatabaseConnection.obtenerConexion();
PreparedStatement ps = con.prepareStatement(sql)
) {
ps.setInt(1, id);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
System.out.println("Error al eliminar producto: " + e.getMessage());
return false;
}
}
// Método privado para mapear una fila del ResultSet a un objeto Producto
private Producto mapearResultado(ResultSet rs) throws SQLException {
return new Producto(
rs.getInt("id"),
rs.getString("nombre"),
rs.getDouble("precio"),
rs.getInt("stock"),
rs.getString("categoria")
);
}
}// Uso del DAO desde el resto de la aplicación
public class Main {
public static void main(String[] args) {
ProductoDAO dao = new ProductoDAO();
// Listar todos
System.out.println("=== TODOS LOS PRODUCTOS ===");
dao.listarTodos().forEach(System.out::println);
// Buscar por ID
System.out.println("\n=== BUSCAR ID 2 ===");
dao.buscarPorId(2).ifPresentOrElse(
System.out::println,
() -> System.out.println("Producto no encontrado")
);
// Insertar
System.out.println("\n=== INSERTAR ===");
Producto nuevo = new Producto(0, "Hub USB 7 puertos", 34.99, 120, "Periféricos");
int nuevoId = dao.insertar(nuevo);
System.out.println("ID generado: " + nuevoId);
// Actualizar
System.out.println("\n=== ACTUALIZAR ===");
Producto actualizado = new Producto(1, "Laptop Pro 15 (2025)", 1149.99, 20, "Electrónica");
System.out.println("Actualizado: " + dao.actualizar(actualizado));
// Eliminar
System.out.println("\n=== ELIMINAR ===");
System.out.println("Eliminado: " + dao.eliminar(nuevoId));
}
}Errores comunes y cómo resolverlos
| Error | Causa probable | Solución |
|---|---|---|
| Communications link failure | MySQL no está corriendo o el host/puerto es incorrecto | Verificar que el servicio MySQL esté activo y revisar la URL de conexión |
| Access denied for user | Usuario o contraseña incorrectos | Verificar las credenciales en MySQL |
| Unknown database | La base de datos no existe | Crear la base de datos con CREATE DATABASE |
| No suitable driver found | El driver MySQL Connector/J no está en el classpath | Agregar la dependencia en Maven/Gradle o el .jar manualmente |
| Table doesn't exist | La tabla no fue creada en la base de datos | Ejecutar el CREATE TABLE correspondiente |
| SSL connection error | Versión de MySQL 8 requiere configuración SSL | Agregar useSSL=false&allowPublicKeyRetrieval=true a la URL |
Buenas prácticas
- Usa siempre PreparedStatement cuando el SQL incluya valores del usuario. Nunca concatenes strings para construir consultas.
- Cierra siempre los recursos: Connection, Statement y ResultSet. Usa try-with-resources para hacerlo automáticamente.
- No guardes las credenciales en el código fuente. Usa variables de entorno, archivos
.propertieso un sistema de configuración. - Usa un pool de conexiones en aplicaciones de producción. Abrir y cerrar una conexión por cada operación es costoso. Bibliotecas como HikariCP gestionan un conjunto de conexiones reutilizables de forma eficiente.
- Separa la lógica de datos con el patrón DAO. El código SQL no debe estar mezclado con la lógica de negocio ni con la interfaz de usuario.
- Maneja los errores con excepciones personalizadas. En lugar de dejar que
SQLExceptionllegue hasta las capas superiores, envuélvela en excepciones con significado de negocio.
Conclusión
Conectar Java con MySQL usando JDBC es una habilidad esencial para construir aplicaciones que persisten datos. Con lo que aprendiste en este artículo puedes establecer conexiones, ejecutar los cuatro tipos de operaciones (SELECT, INSERT, UPDATE, DELETE), manejar transacciones y organizar el código con el patrón DAO.
En proyectos más grandes es común usar frameworks como Spring Data JPA o Hibernate, que añaden una capa de abstracción sobre JDBC y eliminan gran parte del código repetitivo. Pero entender JDBC primero te da el conocimiento de lo que ocurre por debajo, lo cual es invaluable cuando algo falla o necesitas optimizar una consulta.
Para seguir avanzando en el desarrollo backend con Java, te recomendamos leer nuestros artículos sobre introducción a Spring Boot y sobre qué es SQL y para qué sirve, donde encontrarás el contexto necesario para llevar tus aplicaciones Java al siguiente nivel.
No hay comentarios todavía. Sé el primero en compartir tu opinión.