Herramientas de usuario

Herramientas del sitio


apuntes:jdbc

Acceso a Bases de Datos Relacionales

Acceso a una Base de Datos. ¿Qué es JDBC?

JDBC es el interfaz común que Java proporciona para poder conectarnos a cualquier SGBD Relacional con dicho lenguaje de programación. Proporciona una API completa para trabajar con Bases de Datos Relacionales de forma que sea cual sea el motor con el que conectemos, la API siempre será la misma. Simplemente tendremos que hacernos con el Driver correspondiente al motor que queramos usar, que si que dependerá totalmente de éste. A pesar de eso tampoco es mucho problema ya que actualmente podemos encontrar un driver JDBC para prácticamente cualquier SGBDR existente 1).

Ya que, como hemos dicho, el driver es lo único que depende exclusivamente del SGBD utilizado, es muy sencillo escribir aplicaciones cuyo código se pueda reutilizar si más adelante tenemos que cambiar de motor de Base de Datos o bien si queremos permitir que dicha aplicación pueda conectarse a más de un SGBD de forma que el usuario no tenga porque comprometerse a un SGBD concreto si la adquiere o quiere ponerla en marcha.

Operaciones básicas con JDBC

Conectando con la Base de Datos

La conexión con la Base de Datos es la única parte de la programación de aplicaciones con Bases de Datos a través de JDBC que depende directamente del SGBD que se vaya a utilizar. No es un cambio muy grande puesto que simplemente hay que seleccionar el Driver adecuado (que depende de cada SGBD) y la cadena de conexión (que también dependerá). El resto del código para la conexión es el mismo por lo que será muy fácil que esta parte la podemos incluir dentro de una sentencia condicional si queremos que nuestra aplicación soporte varios SGBDs diferentes. Simplemente dependiendo del driver que se haya seleccionado (por ejemplo en la instalación) la aplicación deberá cargar el driver y cadena de conexión adecuado.

A continuación, se pueden ver dichas similitudes para dos casos: el primero el de conecta con una Base de Datos en MySQL y el segundo en PostgreSQL.

Conectando con una Base de Datos de MySQL

. . .
Connection conexion = null;
 
try {
  conexion = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/basededatos", 
    "usuario", "password");
} catch (ClassNotFoundException cnfe} {
  cnfe.prinStackTrace();
} catch (SQLException sqle) {
  sqle.prinStackTrace();
} catch (InstantiationException ie) {
  ie.prinStackTrace();
} catch (IllegalAccessException iae) {
  iae.prinStackTrace();
}
. . .

Conectando con una Base de Datos de PostgreSQL

. . .
Connection conexion = null;
 
try {
  conexion = DriverManager.getConnection(
    "jdbc:postgresql://localhost:5432/basededatos",
    "usuario", "password");
} catch (ClassNotFoundException cnfe} {
  cnfe.prinStackTrace();
} catch (SQLException sqle) {
  sqle.prinStackTrace();
} catch (InstantiationException ie) {
  ie.prinStackTrace();
} catch (IllegalAccessException iae) {
  iae.prinStackTrace();
}
. . .
Ejercicio. ¿Cómo podríamos hacer una aplicación donde el usuario/administrador pueda seleccionar qué tipo de SGBD quiere utilizar?

Desconectar de la Base de Datos

A la hora de desconectar, basta con cerrar la conexión, que será la misma operación independientemente del driver utilizado.

. . .
try {
  conexion.close();
  conexion = null;
} catch (SQLException sqle) {
  sqle.printStackTrace();
}
. . .


Conectarse a un SGBD sin seleccionar una bbdd

Cuando queremos conectarnos a un SGBD para posteriormente seleccionar la base de datos que queremos consultar, podemos hacerlo del siguiente modo:

. . .
//Me conecto a MySql sin indicar ninguna tabla
 
Connection conexion = DriverManager.getConnection("jdbc:mysql://localhost:3306/", 
           "usuario", "password");
 
//Obtengo las bases de datos de ese SGBD
ResultSet catalogo = conexion.getMetaData().getCatalogs();
 
//Muestro los nombres de las bases de datos que existen en ese SGBD
while (catalogo.next()) {
   System.out.println(catalogo.getString(1));
}
catalogo.close();
 
//Cuando sepa a qué base de datos conectarme
conexion.setCatalog("nombreBaseDeDatos");

Consultas sobre la base de datos

Para ejecutar una consulta (INSERT, UPDATE, DELETE o SELECT) emplearemos un objeto de tipo PreparedStatement. Este objeto será creado mediante el método prepareStatement(consulta) de la clase Connection, que recibe un String con la consulta SQL. Además, el objeto PreparedStatement se puede crear a partir de una consulta parametrizada ('?'). Podemos dar valor a esos parámetros posteriormente.

Insertar datos en la base de datos

Ejecutaremos una consulta de tipo INSERT:

. . .
String sentenciaSql = "INSERT INTO productos (nombre, precio) VALUES (?, ?)";
PreparedStatement sentencia = null;
 
try {
  sentencia = conexion.prepareStatement(sentenciaSql);
  sentencia.setString(1, nombreProducto);
  sentencia.setFloat(2, precioProducto);
  sentencia.executeUpdate();
} catch (SQLException sqle) {
  sqle.printStackTrace();
} finally {
  if (sentencia != null)
    try {
      sentencia.close();
    } catch (SQLException sqle) {
      sqle.printStackTrace();
    }
}
. . .

Modificar datos de la base de datos

. . .
String sentenciaSql = "UPDATE productos SET nombre = ?, precio = ? " +
                      "WHERE nombre = ?";
PreparedStatement sentencia = null;
 
try {
  sentencia = conexion.prepareStatement(sentenciaSql);
  sentencia.setString(1, nuevoNombreProducto);
  sentencia.setFloat(2, precioProducto);
  sentencia.setString(3, nombreProducto);
  sentencia.executeUpdate();
} catch (SQLException sqle) {
  sqle.printStackTrace();
} finally {
  if (sentencia != null)
    try {
      sentencia.close();
    } catch (SQLException sqle) {
      sqle.printStackTrace();
    }
}
. . .

Eliminar datos

. . .
String sentenciaSql = "DELETE productos WHERE nombre = ?";
PreparedStatement sentencia = null;
 
try {
  sentencia = conexion.prepareStatement(sentenciaSql);
  sentencia.setString(1, nombreProducto);
  sentencia.executeUpdate();
} catch (SQLException sqle) {
  sqle.printStackTrace();
} finally {
  if (sentencia != null)
    try {
      sentencia.close();
    } catch (SQLException sqle) {
      sqle.printStackTrace();
    }
}
. . .


Consultas de Seleccion

Una consulta de selección sobre una tabla de la base de datos devuelve un conjunto de datos organizados en filas y columnas (registros). En Java almacenamos esa información mediante un objeto de la clase ResultSet, el cual está formado por filas y columnas.

Un ResultSet solo puede recorrerse fila por fila, desde la primera hasta la última en una única dirección deon su método next(). Además, podemos acceder a cualquier columna de cada fila mediante algunos de sus métodos (getInt(), getString(), getObject(), etc). Las columnas se numeran empezando en el nº 1.

. . .
String sentenciaSql = "SELECT nombre, precio FROM productos";
PreparedStatement sentencia = null;
ResultSet resultado = null;
 
try {
  sentencia = conexion.prepareStatement(sentenciaSql);
  resultado = sentencia.executeQuery();
  while (resultado.next()) {
    System.out.println("nombre: " + resultado.getString(1));
    System.out.println("precio: " + resultado.getFloat(2));
  }
} catch (SQLException sqle) {
  sqle.printStackTrace();
} finally {
  if (sentencia != null)
    try {
      sentencia.close();
      resultado.close();
    } catch (SQLException sqle) {
      sqle.printStackTrace();
    }
}
. . .

También se pueden parametrizar la consulta, tal y como se muestra en el siguiente ejemplo donde se mostrará la información de los productos de un determinado precio.

. . .
String sentenciaSql = "SELECT nombre, precio FROM productos " +
                      "WHERE precio = ?";
PreparedStatement sentencia = null;
ResultSet resultado = null;
 
try {
  sentencia = conexion.prepareStatement(sentenciaSql);
  sentencia.setFloat(1, filtroPrecio);
  resultado = sentencia.executeQuery();
  while (resultado.next()) {
    System.out.println("nombre: " + resultado.getString(1));
    System.out.println("precio: " + resultado.getFloat(2));
  }
} catch (SQLException sqle) {
  sqle.printStackTrace();
} finally {
  if (sentencia != null)
    try {
      sentencia.close();
      resultado.close();
    } catch (SQLException sqle) {
      sqle.printStackTrace();
    }
}
. . .

En el caso de las funciones agregadas de SQL (COUNT(), AVG(), MAX(), SUM(), MIN(), etc), podremos tener en cuenta que sólo van a devolver un valor, por lo que no será necesario preparar el código para recorrer el cursor. Podremos acceder directamente a la primera fila del ResultSet y mostrar el resultado, tal y como se muestra en el siguiente ejemplo:

. . .
String sentenciaSql = "SELECT COUNT(*) FROM productos";
PreparedStatement sentencia = null;
ResultSet resultado = null;
 
try {
  sentencia = conexion.prepareStatement(sentenciaSql);
  resultado = sentencia.executeQuery();
  resultado.next();
  System.out.println("Cantidad de productos: " + resultado.getInt(1);
} catch (SQLException sqle) {
  sqle.printStackTrace();
} finally {
  if (sentencia != null)
    try {
      sentencia.close();
      resultado.close();
    } catch (SQLException sqle) {
      sqle.printStackTrace();
    }
}
. . .


Consultas de cualquier tipo

El método execute() se usa cuando la consulta puede devolver ninguno, uno o varios conjuntos de registros ResultSet. Esto se hace para tener un mismo método que permite ejecutar cualquier consulta (acción o selección).

boolean estado = sentencia.execute();
//Si devuelve true es que he obtenido registros
if(estado){
   ResultSet resultado = sentencia.getResultSet();
   //procesamos el resultado
}

Si el método devuelve true, quiere decir que el primer resultado obtenido es un ResultSet (Consulta Select). Podemos acceder a dicho conjunto de registros con el método getResultSet().

Si devuelve false indica que la consulta es del tipo (INSERT, UPDATE o DELETE), o que no ha devuelto resultados. Podemos acceder al número de registros afectados mediante el método getUpdateCount() que devuelve int.

Obtener las columnas de una consulta Select

Si en algún caso al realizar una consulta no sabemos cuántas columnas nos va a devolver, podemos usar el método getMetaData() que devuelve un objeto del tipo ResultSetMetaData.

Existen dos formas de usar dicho método. Se puede invocar desde un objeto de tipo ResultSet obtenido de la ejecución de una consulta:

...  
ResultSet resultado = sentencia.executeQuery();
 
ResultSetMetaData resultadoMetaData = resultado.getMetaData();
int numeroColumnas = resultadoMetaData.getColumnCount();

También puedo obtenerlo antes de ejecutar la consulta llamando al método desde un objeto de tipo PreparedStatement:

...
PreparedStatement sentencia = conexion.preparedStatement(consulta);
 
ResultSetMetaData resultadoMetaData = sentencia.getMetaData();
int numeroColumnas = resultadoMetaData.getColumnCount();

Procedimientos y funciones almacenadas

Procedimientos almacenados

La ejecución de procedimientos almacenados sigue la misma estructura que cualquiera de las sentencias SQL de los ejemplos anteriores, con la excepción de que usaremos la clase CallableStatement para representar al procedimiento y el método execute() de la misma para ejecutarlo.

-- Procedimiento que da de alta una pista, buscando el id del polideportivo
 
DELIMITER ||
CREATE PROCEDURE nueva_pista(p_codigo VARCHAR(10), p_tipo VARCHAR(255), 
    p_precio FLOAT, p_nombre_polideportivo INT)
procedimiento: BEGIN
    DECLARE v_id_polideportivo INT;
 
    SET v_id_polideportivo = (SELECT id FROM polideportivos
                                WHERE nombre = p_nombre_polideportivo);
    IF v_id_polideportivo IS NULL THEN
        LEAVE procedimiento;
    END IF;
 
    INSERT INTO pistas (codigo, tipo, precio, id_polideportivo)
    VALUES (p_codigo, p_tipo, p_precio, p_id_polideportivo);
 
 
END procedimiento; ||
DELIMITER ;
 
CALL nueva_pista('4255-GSF', 'tenis', 2.45, 'Las Rozas'); -- LLamo al procedimiento
. . .
//Método para eliminar todos los productos
 
String sentenciaSql = "call eliminar_todos_los_productos()";
CallableStatement procedimiento = null;
 
try {
  procedimiento = conexion.prepareCall(sentenciaSql);
  procedimiento.execute();
} catch (SQLException sqle) {
  . . .
}
. . .

Y en el caso de que el procedimiento almacenado tuviera algún parámetro:

. . .
//Método para eliminar un producto concreto
String sentenciaSql = "call eliminar_producto(?)";
CallableStatement procedimiento = null;
 
try {
  procedimiento = conexion.prepareCall(sentenciaSql);
  procedimiento.setString(1, nombreProducto);
  procedimiento.execute();
} catch (SQLException sqle) {
  . . .
}
. . .

Funciones almacenadas

En el caso de la ejecución de las funciones almacenadas, seguiremos la misma estructura que con las funciones de agregación de SQL (COUNT(), SUM(), AVG()), ya que nuestras funciones nos devolverán siempre un solo valor (o null en el caso de que no devuelvan nada).

-- Función que devuelve el número de reservas que ha realizado un usuario determinado
 
DELIMITER ||
CREATE FUNCTION get_numero_reservas(p_id_usuario INT)
RETURNS INT
BEGIN
    DECLARE cantidad INT;
    DECLARE existe_usuario INT;
 
    SET existe_usuario = (SELECT COUNT(*) FROM usuarios
                          WHERE id = p_id_usuario);
    IF existe_usuario = 0 THEN
        -- Si el usuario no existe se devuelve valor de error
        RETURN -1;
    END IF;
 
    -- Si todo va bien, se calcula la cantidad y se devuelve
    SET cantidad = (SELECT COUNT(*) FROM reservas R, usuario_reserva UR
        WHERE R.id = UR.id_reserva AND UR.id_usuario = p_id_usuario);
    RETURN cantidad;
 
END; ||
DELIMITER ;
 
SELECT get_numero_reservas(3); -- Muestro la cantidad de reservas del usuario con id = 3

Recordamos que como una función solo devuelve un único valor, no es necesario usar un bucle para recorrer el ResultSet. Bastará con quedarnos con la primera fila.

. . .
String sentenciaSql = "SELECT get_precio_mas_alto()";
PreparedStatement sentencia = null;
ResultSet resultado = null;
 
try {
  sentencia = conexion.prepareStatement(sentenciaSql);
  resultado = sentencia.executeQuery();
  resultado.next();
 
  if (resultado.wasNull)
    System.out.println("No hay datos");
  else {
    float precioMasAlto = resultado.getFloat(1);
    System.out.println("El producto más caro vale " + precioMasAlto);
  }
} catch (SQLException sqle) {
  . . .
}
. . .


Transacciones

En el ámbito de las Bases de Datos, una transacción es cualquier conjunto de sentencias SQL que se ejecutan como si de una sola se tratara. La idea principal es poder ejecutar varias sentencias, que están relacionadas de alguna manera, de forma que si cualquiera de ellas fallara o produjera un error, no se ejecutara ninguna más e incluso se deshicieran todos los cambios que hayan podido efectuar las que ya se habían ejecutado dentro de la misma transacción.

Para ello, tendremos que incorporar tres nuevas instrucciones a las que ya veníamos utilizando hasta ahora. Una instrucción para indicar que comienza una transacción (conexion.setAutoCommit(false)), otra para indicar cuando termina (conexion.commit()) y otra para indicar que la transacción actual debe abortarse y todos los cambios hasta el momento deben ser restaurados al estado anterior (conexion.rollback()).

. . .
String nombreProducto = . . .;
float precioProducto = . . .;
int idCategoria = . . .;
// El id del producto que vamos a registrar aún no se conoce
 
String sqlAltaProducto = "INSERT INTO productos (nombre, precio) VALUES (?, ?)";
String sqlRelacionProducto = 
       "INSERT INTO producto_categoria(id_producto, id_categoria) " +
       "VALUES (?, ?)";
 
try {
  //Inicia transacción
  conextion.setAutoCommit(false);
 
  PreparedStatement sentenciaAltaProducto = 
                    conexion.prepareStatement(sqlAltaProducto,
                    PreparedStatement.RETURNS_GENERATED_KEYS);
  sentenciaAltaProducto.setString(1, nombreProducto);
  sentenciaAltaProducto.setFloat(2, precioProducto);
  sentencia.executeUpdate();
 
  // Obtiene el id del producto que se acaba de registrar
  ResultSet idGenerados = sentenciaAltaProducto.getGeneratedKeys();
  idsGenerados.next();
  int idProducto = idsGenerados.getInt(1);
  sentenciaAltaProducto.close();
 
  PreparedStatement sentenciaRelacionProducto =
                    conexion.prepareStatement(sqlRelacionProducto);
  sentenciaRelacionProducto.setInt(1, idProducto);
  sentenciaRelacionProducto.setInt(2, idCategoria);
  sentenciaRelacionProducto.executeUpdate();
 
  // Valida la transacción
  conexion.commit();
} catch (SQLException sqle) {
  sqle.printStackTrace();
} finally {
  if (sentencia != null)
    try {
      sentencia.close();
      resultado.close();
    } catch (SQLException sqle) {
      sqle.printStackTrace();
    }
}
. . .



Ejercicios

  1. Implementa el sistema de login de una aplicación (usuario, contraseña y nivel de acceso) teniendo en cuenta que la información de los usuarios será una tabla de la base de datos

  2. Realiza una aplicación que cargue el contenido de una tabla de una base de datos en un componente JTable

  3. Realiza una aplicación que cargue el contenido de una tabla de Productos (#id, nombre, descripcion y precio) de una base de datos en un JList, mostrando el nombre y precio (formateado como €)

Proyectos de ejemplo

Todos los proyectos realizados en clase y otros ejemplos están en el repositorio de bitbucket de la unidad 2.

  • Proyectos JDBC Repositorio de proyectos de ejemplo de Santi Faci en Bitbucket

Enlaces de descargas

Prácticas

  • Práctica 2.1 Desarrollar una aplicación que conecta con una Base de Datos Relacional

© 2021 Santiago Faci y Fernando Valdeón

apuntes/jdbc.txt · Última modificación: 2020/09/30 09:15 (editor externo)