Ir al contenido principal

Acceso a bases de datos con JDBC

Introducción

En el desarrollo de aplicaciones modernas, el almacenamiento y la gestión de datos son aspectos fundamentales. Java Database Connectivity (JDBC) es la API estándar que proporciona Java para conectarse e interactuar con bases de datos relacionales, permitiendo ejecutar consultas SQL, actualizar datos y gestionar transacciones. JDBC actúa como un puente entre las aplicaciones Java y los sistemas de gestión de bases de datos (SGBD), ofreciendo una interfaz uniforme para trabajar con diferentes motores de bases de datos como MySQL, PostgreSQL, Oracle o SQL Server.

En este artículo aprenderemos los fundamentos de JDBC, desde la configuración de la conexión hasta la ejecución de operaciones básicas e incluso algunas técnicas avanzadas. Estos conocimientos son esenciales para cualquier desarrollador Java, ya que la mayoría de las aplicaciones empresariales requieren interacción con bases de datos.

Fundamentos de JDBC

Arquitectura de JDBC

JDBC está diseñado como una arquitectura de múltiples capas:

  1. Aplicación Java: Donde escribimos nuestro código que utiliza la API JDBC.
  2. API JDBC: El conjunto de interfaces y clases Java que definen cómo interactuar con las bases de datos.
  3. Controlador JDBC (Driver): Implementa las interfaces de JDBC para un SGBD específico.
  4. Base de datos: El sistema de gestión de bases de datos donde se almacenan los datos.

Componentes principales de JDBC

Los componentes fundamentales de JDBC son:

  • DriverManager: Administra los controladores JDBC disponibles en el sistema.
  • Connection: Representa una conexión con la base de datos.
  • Statement: Permite ejecutar consultas SQL.
  • ResultSet: Almacena los resultados de una consulta.
  • SQLException: Maneja los errores relacionados con la base de datos.

Configuración de JDBC

Añadir el controlador JDBC

Para trabajar con JDBC, primero necesitamos añadir el controlador específico para nuestra base de datos. Si estamos utilizando Maven, podemos agregar la dependencia en el archivo pom.xml:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

Para otros sistemas de gestión de dependencias o instalación manual, descargamos el archivo JAR del controlador y lo añadimos al classpath.

Establecer una conexión

Para conectarnos a una base de datos, necesitamos:

  1. Registrar el controlador JDBC (automático desde Java 6)
  2. Especificar la URL de conexión
  3. Proporcionar credenciales (usuario y contraseña)
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConexionBD {
    public static void main(String[] args) {
        // Datos de conexión
        String url = "jdbc:mysql://localhost:3306/mibasededatos";
        String usuario = "usuario";
        String contrasena = "contraseña";
        
        // Establecer conexión
        try (Connection conexion = DriverManager.getConnection(url, usuario, contrasena)) {
            System.out.println("¡Conexión establecida con éxito!");
            
            // Aquí trabajaríamos con la base de datos
            
        } catch (SQLException e) {
            System.err.println("Error al conectar con la base de datos: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Este código establece una conexión con una base de datos MySQL local llamada "mibasededatos". Utilizamos un bloque try-with-resources para asegurar que la conexión se cierra correctamente, incluso si ocurre una excepción.

Operaciones básicas con JDBC

Consulta de datos (SELECT)

Para recuperar datos de una tabla, utilizamos la sentencia SELECT:

import java.sql.*;

public class ConsultaDatos {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mibasededatos";
        String usuario = "usuario";
        String contrasena = "contraseña";
        
        try (
            Connection conexion = DriverManager.getConnection(url, usuario, contrasena);
            Statement sentencia = conexion.createStatement();
            ResultSet resultado = sentencia.executeQuery("SELECT id, nombre, email FROM usuarios")
        ) {
            System.out.println("Lista de usuarios:");
            while (resultado.next()) {
                int id = resultado.getInt("id");
                String nombre = resultado.getString("nombre");
                String email = resultado.getString("email");
                
                System.out.println(id + ": " + nombre + " (" + email + ")");
            }
            
        } catch (SQLException e) {
            System.err.println("Error en la consulta: " + e.getMessage());
        }
    }
}

Inserción de datos (INSERT)

Para insertar nuevos registros en una tabla:

import java.sql.*;

public class InsertarDatos {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mibasededatos";
        String usuario = "usuario";
        String contrasena = "contraseña";
        
        try (
            Connection conexion = DriverManager.getConnection(url, usuario, contrasena);
            Statement sentencia = conexion.createStatement()
        ) {
            String sql = "INSERT INTO usuarios (nombre, email) VALUES ('Ana López', 'ana@ejemplo.com')";
            int filasAfectadas = sentencia.executeUpdate(sql);
            
            System.out.println("Registro insertado correctamente. Filas afectadas: " + filasAfectadas);
            
        } catch (SQLException e) {
            System.err.println("Error al insertar: " + e.getMessage());
        }
    }
}

Actualización de datos (UPDATE)

Para modificar registros existentes:

import java.sql.*;

public class ActualizarDatos {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mibasededatos";
        String usuario = "usuario";
        String contrasena = "contraseña";
        
        try (
            Connection conexion = DriverManager.getConnection(url, usuario, contrasena);
            Statement sentencia = conexion.createStatement()
        ) {
            String sql = "UPDATE usuarios SET email = 'ana.lopez@ejemplo.com' WHERE nombre = 'Ana López'";
            int filasAfectadas = sentencia.executeUpdate(sql);
            
            System.out.println("Registro actualizado. Filas afectadas: " + filasAfectadas);
            
        } catch (SQLException e) {
            System.err.println("Error al actualizar: " + e.getMessage());
        }
    }
}

Eliminación de datos (DELETE)

Para eliminar registros:

import java.sql.*;

public class EliminarDatos {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mibasededatos";
        String usuario = "usuario";
        String contrasena = "contraseña";
        
        try (
            Connection conexion = DriverManager.getConnection(url, usuario, contrasena);
            Statement sentencia = conexion.createStatement()
        ) {
            String sql = "DELETE FROM usuarios WHERE nombre = 'Ana López'";
            int filasAfectadas = sentencia.executeUpdate(sql);
            
            System.out.println("Registro eliminado. Filas afectadas: " + filasAfectadas);
            
        } catch (SQLException e) {
            System.err.println("Error al eliminar: " + e.getMessage());
        }
    }
}

Técnicas avanzadas con JDBC

PreparedStatement para consultas parametrizadas

Los PreparedStatement ofrecen mejor rendimiento y seguridad frente a ataques de inyección SQL:

import java.sql.*;

public class ConsultaParametrizada {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mibasededatos";
        String usuario = "usuario";
        String contrasena = "contraseña";
        
        String nombreBuscar = "Carlos";
        
        try (
            Connection conexion = DriverManager.getConnection(url, usuario, contrasena);
            PreparedStatement sentencia = conexion.prepareStatement("SELECT * FROM usuarios WHERE nombre LIKE ?")
        ) {
            // Establecer parámetros (índice comienza en 1)
            sentencia.setString(1, "%" + nombreBuscar + "%");
            
            // Ejecutar consulta
            ResultSet resultado = sentencia.executeQuery();
            
            while (resultado.next()) {
                System.out.println("ID: " + resultado.getInt("id"));
                System.out.println("Nombre: " + resultado.getString("nombre"));
                System.out.println("Email: " + resultado.getString("email"));
                System.out.println("--------------------");
            }
            
        } catch (SQLException e) {
            System.err.println("Error en la consulta parametrizada: " + e.getMessage());
        }
    }
}

Transacciones

Las transacciones garantizan que un conjunto de operaciones se complete correctamente o se deshaga por completo:

import java.sql.*;

public class ManejoTransacciones {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mibasededatos";
        String usuario = "usuario";
        String contrasena = "contraseña";
        
        Connection conexion = null;
        
        try {
            conexion = DriverManager.getConnection(url, usuario, contrasena);
            
            // Desactivar auto-commit para manejar transacción manualmente
            conexion.setAutoCommit(false);
            
            // Primera operación
            PreparedStatement sentencia1 = conexion.prepareStatement(
                "INSERT INTO clientes (nombre, email) VALUES (?, ?)"
            );
            sentencia1.setString(1, "Laura Martínez");
            sentencia1.setString(2, "laura@ejemplo.com");
            sentencia1.executeUpdate();
            
            // Segunda operación
            PreparedStatement sentencia2 = conexion.prepareStatement(
                "UPDATE cuentas SET saldo = saldo - ? WHERE id_cliente = ?"
            );
            sentencia2.setDouble(1, 100.0);
            sentencia2.setInt(2, 1);
            sentencia2.executeUpdate();
            
            // Si todo está bien, confirmar transacción
            conexion.commit();
            System.out.println("Transacción completada correctamente");
            
        } catch (SQLException e) {
            System.err.println("Error en la transacción: " + e.getMessage());
            
            // Deshacer cambios en caso de error
            if (conexion != null) {
                try {
                    conexion.rollback();
                    System.out.println("Transacción revertida");
                } catch (SQLException ex) {
                    System.err.println("Error al deshacer la transacción: " + ex.getMessage());
                }
            }
        } finally {
            // Restablecer auto-commit y cerrar conexión
            if (conexion != null) {
                try {
                    conexion.setAutoCommit(true);
                    conexion.close();
                } catch (SQLException e) {
                    System.err.println("Error al cerrar conexión: " + e.getMessage());
                }
            }
        }
    }
}

Manejo de resultados grandes con ResultSet

Para manejar grandes conjuntos de resultados, podemos configurar el tipo y la concurrencia del ResultSet:

import java.sql.*;

public class ResultadosGrandes {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mibasededatos";
        String usuario = "usuario";
        String contrasena = "contraseña";
        
        try (Connection conexion = DriverManager.getConnection(url, usuario, contrasena)) {
            
            Statement sentencia = conexion.createStatement(
                ResultSet.TYPE_SCROLL_INSENSITIVE,  // Permite navegar en ambas direcciones
                ResultSet.CONCUR_READ_ONLY          // Sólo lectura
            );
            
            ResultSet resultado = sentencia.executeQuery("SELECT * FROM productos");
            
            // Ir al último registro para contar el total
            resultado.last();
            int totalFilas = resultado.getRow();
            System.out.println("Total de productos: " + totalFilas);
            
            // Volver al principio para iterar
            resultado.beforeFirst();
            
            // Recorrer los 10 primeros productos
            int contador = 0;
            while (resultado.next() && contador < 10) {
                System.out.println(resultado.getInt("id") + ": " + 
                                  resultado.getString("nombre") + " - " + 
                                  resultado.getDouble("precio") + "€");
                contador++;
            }
            
        } catch (SQLException e) {
            System.err.println("Error al procesar resultados: " + e.getMessage());
        }
    }
}

Patrones de diseño para JDBC

Patrón DAO (Data Access Object)

El patrón DAO separa la lógica de acceso a datos de la lógica de negocio:

// Clase de entidad (POJO)
public class Usuario {
    private int id;
    private String nombre;
    private String email;
    
    // Constructor, getters y setters
    public Usuario(int id, String nombre, String email) {
        this.id = id;
        this.nombre = nombre;
        this.email = email;
    }
    
    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 getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    @Override
    public String toString() {
        return "Usuario{id=" + id + ", nombre='" + nombre + "', email='" + email + "'}";
    }
}

// Interfaz DAO
public interface UsuarioDAO {
    Usuario obtenerPorId(int id) throws SQLException;
    List<Usuario> obtenerTodos() throws SQLException;
    void insertar(Usuario usuario) throws SQLException;
    void actualizar(Usuario usuario) throws SQLException;
    void eliminar(int id) throws SQLException;
}

// Implementación DAO con JDBC
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class UsuarioDAOImpl implements UsuarioDAO {
    private String url;
    private String usuario;
    private String contrasena;
    
    public UsuarioDAOImpl(String url, String usuario, String contrasena) {
        this.url = url;
        this.usuario = usuario;
        this.contrasena = contrasena;
    }
    
    private Connection obtenerConexion() throws SQLException {
        return DriverManager.getConnection(url, usuario, contrasena);
    }
    
    @Override
    public Usuario obtenerPorId(int id) throws SQLException {
        Usuario usuario = null;
        String sql = "SELECT * FROM usuarios WHERE id = ?";
        
        try (
            Connection conexion = obtenerConexion();
            PreparedStatement sentencia = conexion.prepareStatement(sql)
        ) {
            sentencia.setInt(1, id);
            ResultSet resultado = sentencia.executeQuery();
            
            if (resultado.next()) {
                usuario = new Usuario(
                    resultado.getInt("id"),
                    resultado.getString("nombre"),
                    resultado.getString("email")
                );
            }
        }
        
        return usuario;
    }
    
    @Override
    public List<Usuario> obtenerTodos() throws SQLException {
        List<Usuario> usuarios = new ArrayList<>();
        String sql = "SELECT * FROM usuarios";
        
        try (
            Connection conexion = obtenerConexion();
            Statement sentencia = conexion.createStatement();
            ResultSet resultado = sentencia.executeQuery(sql)
        ) {
            while (resultado.next()) {
                usuarios.add(new Usuario(
                    resultado.getInt("id"),
                    resultado.getString("nombre"),
                    resultado.getString("email")
                ));
            }
        }
        
        return usuarios;
    }
    
    @Override
    public void insertar(Usuario usuario) throws SQLException {
        String sql = "INSERT INTO usuarios (nombre, email) VALUES (?, ?)";
        
        try (
            Connection conexion = obtenerConexion();
            PreparedStatement sentencia = conexion.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)
        ) {
            sentencia.setString(1, usuario.getNombre());
            sentencia.setString(2, usuario.getEmail());
            sentencia.executeUpdate();
            
            // Obtener ID generado
            ResultSet generatedKeys = sentencia.getGeneratedKeys();
            if (generatedKeys.next()) {
                usuario.setId(generatedKeys.getInt(1));
            }
        }
    }
    
    @Override
    public void actualizar(Usuario usuario) throws SQLException {
        String sql = "UPDATE usuarios SET nombre = ?, email = ? WHERE id = ?";
        
        try (
            Connection conexion = obtenerConexion();
            PreparedStatement sentencia = conexion.prepareStatement(sql)
        ) {
            sentencia.setString(1, usuario.getNombre());
            sentencia.setString(2, usuario.getEmail());
            sentencia.setInt(3, usuario.getId());
            sentencia.executeUpdate();
        }
    }
    
    @Override
    public void eliminar(int id) throws SQLException {
        String sql = "DELETE FROM usuarios WHERE id = ?";
        
        try (
            Connection conexion = obtenerConexion();
            PreparedStatement sentencia = conexion.prepareStatement(sql)
        ) {
            sentencia.setInt(1, id);
            sentencia.executeUpdate();
        }
    }
}

// Uso del DAO
import java.sql.SQLException;
import java.util.List;

public class EjemploDAO {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mibasededatos";
        String usuarioBD = "usuario";
        String contrasenaBD = "contraseña";
        
        UsuarioDAO dao = new UsuarioDAOImpl(url, usuarioBD, contrasenaBD);
        
        try {
            // Insertar usuario
            Usuario nuevo = new Usuario(0, "Pedro García", "pedro@ejemplo.com");
            dao.insertar(nuevo);
            System.out.println("Usuario insertado con ID: " + nuevo.getId());
            
            // Obtener todos los usuarios
            List<Usuario> usuarios = dao.obtenerTodos();
            System.out.println("Lista de usuarios:");
            for (Usuario u : usuarios) {
                System.out.println(u);
            }
            
            // Actualizar usuario
            Usuario actualizar = dao.obtenerPorId(nuevo.getId());
            if (actualizar != null) {
                actualizar.setEmail("pedro.garcia@ejemplo.com");
                dao.actualizar(actualizar);
                System.out.println("Usuario actualizado");
            }
            
            // Eliminar usuario
            dao.eliminar(nuevo.getId());
            System.out.println("Usuario eliminado");
            
        } catch (SQLException e) {
            System.err.println("Error en operaciones DAO: " + e.getMessage());
        }
    }
}

Patrón Singleton para conexiones

El patrón Singleton puede ayudar a gestionar una única instancia de la conexión:

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

public class ConexionSingleton {
    private static ConexionSingleton instancia;
    private Connection conexion;
    
    private String url = "jdbc:mysql://localhost:3306/mibasededatos";
    private String usuario = "usuario";
    private String contrasena = "contraseña";
    
    private ConexionSingleton() {
        try {
            conexion = DriverManager.getConnection(url, usuario, contrasena);
        } catch (SQLException e) {
            System.err.println("Error al crear la conexión: " + e.getMessage());
        }
    }
    
    public static synchronized ConexionSingleton obtenerInstancia() {
        if (instancia == null) {
            instancia = new ConexionSingleton();
        }
        return instancia;
    }
    
    public Connection getConexion() {
        return conexion;
    }
    
    public void cerrarConexion() {
        if (conexion != null) {
            try {
                conexion.close();
            } catch (SQLException e) {
                System.err.println("Error al cerrar la conexión: " + e.getMessage());
            }
        }
    }
}

Mejores prácticas con JDBC

  1. Siempre cerrar recursos:

    • Utiliza try-with-resources para garantizar que los recursos se cierren correctamente.
    • Cierra ResultSet, Statement y Connection en el orden correcto.
  2. Utilizar pool de conexiones:

    • Para aplicaciones con muchos usuarios, considera utilizar un pool de conexiones como HikariCP, Apache DBCP o C3P0.
    • Esto mejora el rendimiento al reutilizar conexiones en lugar de crear nuevas.
  3. Parametrizar consultas:

    • Utiliza siempre PreparedStatement en lugar de concatenar strings para formar consultas SQL.
    • Esto evita ataques de inyección SQL y mejora el rendimiento.
  4. Utilizar transacciones:

    • Agrupa operaciones relacionadas en transacciones para mantener la integridad de los datos.
    • Asegúrate de manejar correctamente los commits y rollbacks.
  5. Separar responsabilidades:

    • Implementa el patrón DAO para separar la lógica de acceso a datos.
    • Considera usar frameworks como JPA/Hibernate para operaciones más complejas.

Resumen

JDBC es una API fundamental para la interacción entre aplicaciones Java y bases de datos relacionales. Proporciona un conjunto de clases e interfaces que permite realizar operaciones básicas como inserción, consulta, actualización y eliminación de datos, así como operaciones más avanzadas como el manejo de transacciones y consultas parametrizadas.

En este artículo hemos aprendido a establecer conexiones con bases de datos, ejecutar consultas SQL, manejar resultados y aplicar patrones de diseño como DAO y Singleton para mejorar la estructura y mantenibilidad de nuestras aplicaciones. La comprensión de JDBC es esencial para cualquier desarrollador Java, ya que proporciona los cimientos para trabajar con datos persistentes en aplicaciones empresariales.