UD8. Uso de BBDD
1
2
1. Introducción. Objetivos
3
1. Introducción
Las aplicaciones informáticas suelen necesitar manejar grandes volúmenes de datos, para ello, las bases de datos relacionales son una opción común debido a su amplia adopción. Estas bases de datos organizan la información en tablas, donde cada fila representa un registro y cada columna un atributo. Es esencial tener una clave primaria para identificar de forma única cada registro.
El SGBD (Sistema Gestor de Bases de Datos) administra el almacenamiento, mantenimiento y recuperación de los datos.
JDBC (Java Database Connectivity) es una API en Java que simplifica el acceso a las bases de datos relacionales. Facilita la comunicación con diferentes motores de bases de datos mediante un único mecanismo, permitiendo la construcción de sentencias SQL dentro de aplicaciones Java de manera sencilla.
4
1. Introducción. JDBC
La API de JDBC, ubicada en el paquete java.sql, está compuesta por diversas clases que trabajan de manera conjunta. No es necesario conocer todas, pero es fundamental estar familiarizado con las principales:
5
2. JDBC
6
2. JDBC. Driver
Cada fabricante de un SGBD desarrolla su producto utilizando mecanismos propios, como protocolos, llamadas y API de bajo nivel, para establecer una conexión con la base de datos y permitir el acceso a sus servicios. Sin embargo, las clases que forman parte de la API de JDBC no conocen estos detalles específicos de cada producto, como MySQL, Oracle Database o PostgreSQL.
Entre las clases de la API de JDBC, hay una clase especial llamada Driver, que es específica de cada SGBD. De hecho, cuando un fabricante desarrolla un nuevo SGBD, también desarrolla el driver correspondiente que permite su uso con JDBC. Este driver JDBC se añadirá a nuestro programa según el SGBD que hayamos elegido.
7
2. JDBC. Driver
De esta manera, el driver posibilita que cualquier aplicación Java, a través de la API JDBC, pueda aprovechar los servicios de cualquier SGBD para el cual exista un driver JDBC disponible. Esto elimina la necesidad de que cada aplicación Java tenga que comprender los detalles internos específicos de cada SGBD.
Agregar un driver a nuestro proyecto implica incluir la biblioteca que contiene la clase correspondiente al driver, es decir, seleccionar el fichero .jar que incluye el driver adecuado.
8
2. JDBC. Conexión
Antes de interactuar con la base de datos, es esencial establecer una conexión entre nuestra aplicación y el Sistema de Gestión de Base de Datos (SGBD). Esta conexión actúa como un conducto que facilita la comunicación entre ambas partes. Permite que las consultas SQL viajen desde la aplicación al SGBD y que los resultados de estas consultas se devuelvan en sentido contrario. Es importante mantener abierta esta conexión mientras sea necesaria. Una vez que ya no se requiera, es fundamental cerrarla. Si una conexión permanece abierta sin motivo, seguirá consumiendo recursos del SGBD.
Para establecer una conexión, podemos utilizar el método estático getConnection de DriverManager. Este método toma tres parámetros:
El método devuelve un objeto de tipo Connection, que representa la conexión establecida entre nuestra aplicación y la base de datos.
9
2. JDBC. Conexión
Suponiendo que nuestra base de datos se llama "academia" y que tenemos un usuario llamado "Pepe" con la contraseña "12345", podemos establecer la conexión de la siguiente manera:
Cada conexión actúa como una tubería que permite el flujo de distintos objetos. Estos objetos representan información relevante tanto para el Sistema de Gestión de Base de Datos como para la aplicación.
10
2. JDBC. Conexión
El método getConnection() puede lanzar alguna de las siguientes excepciones si se produce algún error al crear la conexión:
Cuando ya no necesitemos una conexión, es importante cerrarla utilizando el método close():
Este método libera los recursos asociados con la conexión y la devuelve al pool de conexiones o la cierra definitivamente, dependiendo de la configuración y del tipo de conexión utilizada.
11
2. JDBC. Conexión
Crea un programa en Java que se conecte a tu servidor de MySQL utilizando el conector, ruta, base de datos, usuario y contraseña adecuado.
Si la conexión es satisfactoria se debe mostrar un mensaje por consola, si no es así, capturar una excepción SQLException que muestre el error.
12
2. JDBC. Ejecución de sentencias
Para ejecutar una sentencia SQL (SELECT, INSERT, UPDATE o DELETE), necesitaremos utilizar un objeto de la clase Statement. Al igual que con la conexión, estos objetos no se crean directamente mediante el operador new, sino a través de métodos que se encargan de construir, configurar y devolver los objetos necesarios.
La sintaxis simplificada de la sentencia SELECT es:
SELECT <lista_de_campos> *
FROM <tabla> <join>
[WHERE <condición>]
[ORDER BY <lista_de_campos>]
También podemos usar la sentencia SELECT para crear subconsultas, funciones de agregados o agrupamientos
La sintaxis básica de INSERT es:
INSERT INTO <tabla> [(<lista_de_campos>)]
VALUES (<valor1>, <valor2>, ...)
13
2. JDBC. Ejecución de sentencias
La sintaxis de UPDATE es:
UPDATE <tabla>
SET <campo1> = <valor1>,
<campo2> = <valor2>
[WHERE <condición>]
La sintaxis de DELETE es:
DELETE
FROM <tabla>
[WHERE <condición>]
Estas son las estructuras básicas de las sentencias SQL más comunes. Cada una puede variar en complejidad dependiendo de los requisitos específicos de la consulta.
14
2. JDBC. Ejecución de sentencias
Para comenzar, crearemos un objeto de tipo Statement de la siguiente manera:
Statement sentencia = conexion.createStatement();
Recuerda que el objeto conexion es de tipo Connection y representa la conexión establecida entre la aplicación y el SGBD.
Vamos a utilizar dos métodos para ejecutar sentencias SQL:
15
2. JDBC. Ejecución de sentencias
El método executeQuery() de la clase Statement se utiliza para ejecutar una consulta y devuelve el resultado a través de un objeto de tipo ResultSet. Su prototipo es el siguiente:
ResultSet executeQuery(String sql)
Veamos un ejemplo de cómo consultar la tabla Alumnos:
String sql = "SELECT * FROM alumnos";
Statement sentencia = conexion.createStatement();
ResultSet rs = sentencia.executeQuery(sql);
Una vez obtenidos los resultados de la consulta, encapsulados en un objeto de tipo ResultSet, podemos proceder a trabajar con ellos. Al igual que la mayoría de las clases de la API de JDBC, el método executeQuery() puede lanzar dos excepciones en caso de que surja algún problema (SQLException y SQLTimeoutException).
16
2. JDBC. Ejecución de sentencias
La clase ResultSet funciona de manera similar a un iterador. Posee un cursor que apunta en cada momento a una única fila, conocida como fila activa. Este sistema permite acceder únicamente a los datos de la fila activa; para trabajar con todos los datos del objeto ResultSet, debemos desplazar el cursor de fila en fila.
Cuando se crea un objeto ResultSet, el cursor se posiciona inicialmente delante de la primera fila. Por lo tanto, es necesario avanzar el cursor por primera vez para situarlo en la primera fila y comenzar a trabajar con los datos de esta.
17
2. JDBC. Ejecución de sentencias
Para desplazarse de una fila a la siguiente en un ResultSet, se utiliza siguiente método:
El valor booleano que devuelve next() es crucial para determinar cuándo hemos terminado de procesar todas las filas de un ResultSet. Es común utilizar rs.next() como condición de un bucle while.
18
2. JDBC. Ejecución de sentencias
Ahora podemos extraer los datos de la fila activa. Para ello, disponemos de los siguientes métodos:
Todos los métodos para extraer los datos de una tabla están sobrecargados para que, en lugar de especificar el nombre del campo, se pueda indicar su posición en la consulta. Es importante tener en cuenta que ResultSet enumera los campos de una consulta comenzando en 1.
19
2. JDBC. Ejecución de sentencias
Crea un programa que muestre la información de todos los alumnos. Utiliza la base de datos academia.sql.
20
2. JDBC. Movimientos del cursor
El objeto ResultSet que hemos utilizado se puede denominar por defecto, ya que es de solo lectura (se pueden extraer los datos del objeto ResultSet, pero no modificarlos) y su cursor siempre avanza hacia delante. Sin embargo, es posible utilizar otros tipos de ResultSet que permiten la modificación de sus datos y mover el cursor hacia delante o atrás, así como posicionarlo en cualquier fila.
Para obtener otros tipos de ResultSet es necesario crear los objetos Statement mediante el método sobrecargado de Connection:
Statement createStatement(int tipoResultSet, int concurrencia);
El parámetro tipoResultSet admite cualquiera de las constantes:
21
2. JDBC. Movimientos del cursor
El segundo parámetro indica la posibilidad de modificar los datos contenidos en el objeto ResultSet:
Cuando usamos el método createStatement() sin parámetros, los objetos ResultSet obtenidos por defecto serán de tipo TYPE_FORWARD_ONLY y CONCUR_READ_ONLY.
22
2. JDBC. Movimientos del cursor
Todos los métodos que mueven el cursor devuelven un booleano que indica si ha sido posible desplazar el cursor (true) o, por el contrario, el movimiento del cursor no puede realizarse (en este caso devuelven false). Un movimiento del cursor no podrá llevarse a cabo si la fila especificada no existe o el tipo de cursor no permite ese tipo de movimiento. Estos métodos son:
23
2. JDBC. Movimientos del cursor
La capacidad de mover el cursor en un ResultSet puede llevarnos a perder la noción de su ubicación actual. Por esta razón, existen una serie de métodos que nos permiten verificar si el cursor se encuentra en posiciones específicas. Estos métodos nos hacen preguntas como: ¿Está el cursor en cierta posición? Todos estos métodos devuelven un valor booleano para indicar sí o no.
24
2. JDBC. INSERT, UPDATE, DELETE
En este caso, la ejecución de cualquiera de estas sentencias no devuelve un conjunto de datos como resultado, sino que simplemente realizan la operación indicada en el servidor. Para estas operaciones, disponemos del método executeUpdate(String sql), que ejecuta la sentencia SQL correspondiente y devuelve el número de filas que han sido afectadas por la operación. Es decir, el número de registros que se han eliminado, actualizado o insertado.
Aunque el nombre del método pueda llevar a confusión, es importante destacar que este método sirve tanto para insertar y actualizar como para eliminar registros en una tabla.
String sqlUpdate = "UPDATE alumnos SET nombre ='Pepico' WHERE dni LIKE '12345678A';
Statement sentenciaUpdate = conexion.createStatement();
System.out.println(sentenciaUpdate.executeUpdate(sqlUpdate));
25
2. JDBC. INSERT, UPDATE, DELETE
Inserta un alumno nuevo en la tabla “alumno”.
26
3. SQL Injection y sentencias parametrizadas
27
3. SQL Injection y sentencias parametrizadas
El SQL Injection es una técnica de hacking que Implica inyectar código SQL en una consulta a través de la entrada de datos. Supongamos que tenemos una aplicación que, en cierto momento, nos solicita el nombre de un alumno para eliminarlo. El código que podríamos utilizar se vería así:
String nombre;
nombre = new Scanner(System.in).next(); // pedimos el nombre al usuario
String sql = "DELETE FROM Alumnos WHERE nombre = '" + nombre + "'";
Si el usuario introduce el nombre "Perico Pérez", la consulta ejecutada en el servidor de la base de datos sería:
DELETE FROM Alumnos WHERE nombre = 'Perico Pérez'
Esto provocaría la eliminación del alumno con ese nombre. Sin embargo, si el usuario tiene conocimientos de SQL y introduce de manera malintencionada código SQL como si fuera el nombre del alumno, ese código se agregaría a la sentencia. Por ejemplo, si alguien quisiera eliminar completamente nuestra base de datos podría ingresar:
xxx' OR '1' = '1
La cadena ingresada por el usuario se combinaría con la nuestra, resultando en:
DELETE FROM Alumnos WHERE nombre = 'xxx' OR '1' = '1'
Como la cadena '1' siempre es igual a '1', esto causaría la eliminación de todos los registros de la tabla Alumnos.
28
3. SQL Injection y sentencias parametrizadas
El SQL Injection no se limita a la eliminación de datos, también puede inyectar código malicioso en otros contextos. Por ejemplo, en una consulta que muestre todos los alumnos de un curso:
String curso;
curso = new Scanner(System.in).nextLine(); // pedimos el curso al usuario
String sql = "SELECT * FROM Alumnos WHERE curso='" + curso + "'";
Si al solicitar el curso al usuario, este ingresa:
xxx ' ; DROP TABLE Alumnos; SELECT * FROM Alumnos WHERE curso= 'x
La cadena enviada al SGBD para su ejecución estará compuesta de tres sentencias, una de ellas será la eliminación completa de una tabla:
SELECT * FROM Alumnos WHERE curso = 'xxx'
DROP TABLE Alumnos
SELECT * FROM Alumnos WHERE curso = 'x '
Este tipo de ataque podría tener graves consecuencias, como la eliminación de la tabla Alumnos. Es importante tener cuidado con la entrada de datos y utilizar medidas de seguridad para prevenir el SQL Injection.
29
3. SQL Injection y sentencias parametrizadas
Como se ha visto, para evitar la inyección de SQL, no es recomendable construir sentencias mediante la concatenación de cadenas con datos proporcionados por el usuario. En su lugar, se deben utilizar sentencias parametrizadas, que son aquellas que contienen marcadores o parámetros que serán reemplazados por valores. Este enfoque permite adaptar y reutilizar la misma consulta varias veces. En JDBC, la interfaz PreparedStatement representa una consulta parametrizada.
Veamos el concepto de consulta parametrizada con un ejemplo: si deseamos consultar a todos los alumnos de un curso específico cuya nota media es superior a cierto valor de corte. La consulta parametrizada se formaría de la siguiente manera:
SELECT * FROM Alumnos WHERE curso = ? AND media > ?
En la consulta, se utiliza el símbolo de interrogación (?) para indicar la introducción de un parámetro. Si queremos que el curso sea "1A" y la nota de corte sea 6.0, podemos asignar estos valores a los parámetros:
El primer parámetro: ? = "1A". El segundo parámetro: ? = 6.0.
De esta forma, la consulta quedaría así:
SELECT * FROM Alumnos WHERE curso = '1A' AND media > 6.0
Este enfoque ayuda a prevenir la inyección de SQL y hace que las consultas sean más seguras y eficientes.
30
3. SQL Injection y sentencias parametrizadas
Una ventaja considerable de utilizar consultas con parámetros es que no es necesario preocuparse por las comillas, ya que solo hay que asignar valores a los parámetros y JDBC se encarga automáticamente de entrecomillar los campos según sea necesario. Además, si necesitas reutilizar la consulta, simplemente asignas nuevos valores a los parámetros.
Veamos el código necesario para utilizar consultas con parámetros:
// Consulta con parámetros. Cada parámetro se indica con ?
String sql = "SELECT nombre, media FROM Alumnos WHERE curso = ? AND media > ?";
// Creamos un objeto de tipo PreparedStatement
PreparedStatement sentencia = con.prepareStatement(sql);
// Asignamos los parámetros
sentencia.setString(1, curso); // El primer ? corresponde al curso
sentencia.setDouble(2, notaCorte); // El segundo ? corresponde a la nota de corte
ResultSet rs = sentencia.executeQuery(); // Ejecutamos la consulta
31
3. SQL Injection y sentencias parametrizadas
Para asignar los parámetros, disponemos de métodos como:
void setString(int indiceParametro, String valor)
void setInt(int indiceParametro, int valor)
void setDouble(int indiceParametro, double valor)
void setBoolean(int indiceParametro, boolean valor)
void setDate(int indiceParametro, Date valor)
En todos los métodos, el primer parámetro indica el índice del parámetro de la consulta (los parámetros se comienzan a contar desde 1), y el segundo parámetro es el valor que se asignará. Existen tantos métodos como tipos de datos, los mencionados aquí son solo algunos ejemplos.
32
3. SQL Injection y sentencias parametrizadas
Crea un programa que muestre todos los alumnos de una asignatura cuya nota es mayor que cierta nota de corte. El nombre de la asignatura y la nota serán introducidos por el usuario. Implementa un método para esta consulta, y repite tantas veces la consulta como veces quiera el usuario.
33
4. Operaciones CRUD
34
4. Operaciones CRUD
CRUD son las siglas en inglés de "Crear, Leer, Actualizar y Borrar", y se utilizan para referirse a las operaciones básicas que se realizan en una base de datos.
Hasta ahora, hemos estado trabajando con datos aislados, como simples variables. Esto nos ha permitido familiarizarnos con la API de JDBC y el manejo de una base de datos. Sin embargo, esta forma de trabajar no es la más común. A partir de ahora, trabajaremos con clases y objetos (como la clase Persona, la clase Mascota, etc.), a los cuales incorporaremos las operaciones CRUD, que se encargarán de gestionar el objeto en la base de datos.
Existe una técnica llamada mapeo objeto-relacional, que consiste en vincular cada atributo de una clase con un campo de una tabla en la base de datos. Es decir, convertimos los datos representados como objetos en registros de una tabla relacional, y viceversa.
35
4. Operaciones CRUD
A cada clase de nuestra aplicación se le añadirán los siguientes métodos:
Puedes ver un ejemplo para la clase Alumno aquí:
36
4. Operaciones CRUD
Crea la clase Profesor siguiendo la misma técnica CRUD.
37
4. Operaciones CRUD. Clases DAO
Desde una perspectiva de abstracción en la Programación Orientada a Objetos (POO), un enfoque purista argumentará que una clase debe encapsular sus características y comportamientos esenciales. Por ejemplo, en el caso de un alumno, la clase debería manejar sus datos personales, sus notas, sus matrículas, etc. Sin embargo, es ampliamente aceptado que las operaciones CRUD en una base de datos no son inherentes al mundo de un alumno.
Agregar métodos relacionados con operaciones de base de datos a una clase como Alumno puede considerarse una distorsión de la abstracción que intenta representar la realidad. Por esta razón, existe un enfoque en el que cada clase de interés en nuestra aplicación se diseña exclusivamente con base en la abstracción del mundo real. Si es necesario que esa clase se almacene en una base de datos, se diseña una segunda clase, llamada DAO (por sus siglas en inglés "Data Access Object"), con la única misión de realizar las operaciones CRUD en la base de datos para su clase correspondiente.
38
4. Operaciones CRUD. Clases DAO
Clase Alumno, que solo contiene atributos y métodos producto de la abstracción de los alumnos en la realidad; y clase AlunmoDAO, que está diseñada para interactuar con objeto de tipo Alumno con la BD. Cada clase DAO tendrá los mismos métodos, pero estará diseñada específicamente para una clase del modelo de dominio.
Puedes ver un ejemplo para la clase Alumno aquí:
https://github.com/JMProf/Prog/blob/main/BBDD/AlumnoDAO.java
39
5. Despliegue con Docker
40
5. Despliegue con Docker
Docker es una herramienta que permite empaquetar aplicaciones junto con todas sus dependencias (como bases de datos, librerías o configuraciones) en contenedores ligeros y portables. Esto garantiza que el programa se ejecute de la misma forma en cualquier entorno, sin necesidad de configuraciones complicadas o instalaciones adicionales.
Gracias a Docker, podemos desplegar nuestra aplicación Java y su base de datos MySQL de manera sencilla, pues no requiere que los usuarios instalen manualmente las dependencias de nuestro programa (como instalar el SGBD, crear usuarios, bases de datos…).
41
5. Despliegue con Docker
Realiza la siguiente práctica donde empaquetarás un sencillo programa Java que utilice una base de datos con MySQL:
42
6. SQLite
43
6. SQLite
SQLite es un SGBD que guarda las bases de datos en un fichero. Al contrario que MySQL, no es un SGBD de tipo cliente-servidor, por lo que el motor de SQLite no es un proceso independiente con el que se comunique nuestro programa Java.
En su página oficial se dice que es recomendable utilizar un SGBD tipo cliente-servidor en los siguientes casos:
Para el resto de casos, recomienda utilizar SQLite.
44
6. SQLite
Algunos ejemplos de uso son:
Para utilizarlo con Java, será necesario utilizar la librería que contenga el conector para el JDBC, puedes encontrarlo en el siguiente enlace:
https://github.com/xerial/sqlite-jdbc/releases/tag/3.49.1.0
Para utilizarlo en tu programa Java, además de importar la librería, puedes utilizar la dirección jdbc:sqlite:miBaseDeDatos.db para el DriverManager.
45
6. SQLite
Descarga la base de datos libros.db disponible en el siguiente enlace, guárdala en la carpeta del proyecto y haz una consulta de la tabla “libros”.
https://github.com/nekrum/Ejercicios-SQLite/tree/master/bases_de_datos
46