Saltar al contenido principal
Intermedio CódigoTutorial

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.

Fran Cobos 10 min de lectura 1879 palabras

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:

  1. Configurar el proyecto con Maven y el conector MySQL
  2. Crear la conexión JDBC correctamente (sin fugas de memoria)
  3. CRUD completo con PreparedStatement (seguro contra SQL injection)
  4. Connection pool con HikariCP (lo que usan las empresas)
  5. 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:

HerramientaVersión mínimaComprobar con
Java JDK17+java --version
Maven3.9+mvn --version
MySQL Server8.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

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

ConceptoQué usarPor qué
ConexiónDriverManager o HikariCPDriverManager para aprender, HikariCP para producción
ConsultasPreparedStatementPreviene SQL injection, obligatorio
Recursostry-with-resourcesCierra conexiones automáticamente
PatrónDAOSepara 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.

Fran Cobos

Fran Cobos

Desarrollador Full Stack especializado en IA aplicada, automatización y desarrollo web. Escribo sobre herramientas, tutoriales y casos reales para programadores.

¿Necesitas desarrollo a medida?

Apps web, IA, módulos ERP — cuéntame tu proyecto.