Qué es una base de datos
Una base de datos es una entidad en la cual se pueden almacenar datos de manera estructurada y con la menor redundancia posible. Y hay una segunda parte en la definición que explica para qué sirve: distintos programas y distintos usuarios deben poder usar esos datos. Por eso el concepto de base de datos suele estar relacionado con el de red: la información se tiene que poder compartir.
«Sistema de información» es el término general que se usa para la estructura global, que incluye todos los mecanismos que se instalaron para compartir esos datos.
Una base de datos le da a quien la usa acceso a los datos: los puede visualizar, ingresar o actualizar, de acuerdo con los derechos de acceso que se le hayan otorgado. Y se vuelve más útil a medida que la cantidad de datos almacenados crece. Puede ser:
- Local, cuando la usa un solo usuario en un equipo.
- Distribuida, cuando la información se almacena en equipos remotos y se accede a ella a través de una red.
La ventaja principal de usar bases de datos es que varios usuarios pueden acceder al mismo tiempo. Eso es lo que las diferencia de guardar los datos en un archivo, que es lo que se hizo en la lección 4: un archivo lo usa un programa por vez, no tiene forma de darle permisos distintos a cada usuario y no puede evitar que dos personas lo pisen.
Los gestores y los modelos de datos
En la era digital surgió enseguida la necesidad de contar con un sistema de administración para controlar tanto los datos como los usuarios. Esa administración se hace con un DBMS (Database Management System), que en español se llama SGBD: Sistema Gestor de Base de Datos.
Un gestor es un conjunto de servicios —aplicaciones de software— que permite:
- Un fácil acceso a los datos.
- El acceso a la información por parte de múltiples usuarios.
- La manipulación de los datos que están en la base: insertar, eliminar, editar, buscar.
Las bases de datos aparecieron a finales de la década de 1960, cuando hizo falta un sistema de administración de información flexible. Los distintos modelos de DBMS se distinguen por cómo representan los datos almacenados:
| Modelo | Cómo organiza los datos |
|---|---|
| Jerárquico | Los datos se organizan jerárquicamente mediante un árbol invertido, y se usan punteros para navegar. Fue el primer modelo de DBMS. Un ejemplo clásico: una Sucursal que tiene Oficinas, y cada oficina tiene Empleados, y cada empleado tiene Hijos. |
| De red | También usa punteros, pero no necesariamente un árbol invertido: un mismo dato puede alcanzarse por más de un camino. Por ejemplo, las Ciudades se relacionan con Vendedores, y los vendedores con Productos, sin que la estructura sea un árbol. |
| Relacional (RDBMS) | Es el que se usa hoy en casi todos los casos: los datos se almacenan en tablas de dos dimensiones (filas y columnas) y se manipulan según la teoría relacional de las matemáticas. |
| Deductivo | Los datos se representan como una tabla, pero se manipulan mediante cálculos de predicados: permite derivar información nueva a partir de la que se cargó explícitamente. |
| Orientado a objetos (ODBMS) | Los datos se almacenan como objetos, con estructuras del tipo de las clases que muestran los datos que contienen. Es el modelo más cercano a la programación que se estuvo viendo en las lecciones anteriores. |
Las bases de datos relacionales
Hay un dato que conviene tener presente: la inmensa mayoría de los DBMS administran bases de datos relacionales. Son las que organizan los datos en tablas que después se relacionan mediante campos clave, y trabajan con el lenguaje estándar SQL.
Cada tabla es una serie de filas y columnas, donde:
- Cada fila es un registro: un elemento de la tabla —la señora Eva Jiménez, el señor Ricardo Gómez—.
- Cada columna es un campo: un dato de esos elementos —nombre, DNI—.
Y una regla que es la base de todo: no puede aparecer dos veces el mismo registro. Para garantizarlo, uno o más campos forman lo que se conoce como clave principal. La clave principal no se puede repetir en dos registros, y es lo que permite que los datos se relacionen.
SQL es el lenguaje estándar internacional que se usa para consultar y manipular los datos de una base relacional. Es el mismo lenguaje que se ve en la materia Base de Datos: acá no se enseña SQL, se usa.
Los gestores relacionales que existen
Algunos de los RDBMS actuales son: Oracle Database, IBM DB2, Microsoft SQL Server, MySQL, PostgreSQL, MongoDB, MariaDB, Borland Paradox, Sybase y SQLite.
El apunte menciona además que el JDK incluía un RDBMS puro de Java llamado Java DB —que es la distribución de Apache Derby—. Conviene saber que las versiones modernas del JDK ya no lo traen: para practicar lo que sigue se usa un gestor instalado aparte.
Aunque la lista sea larga, hay dos que importan acá: MySQL y su bifurcación MariaDB son los sistemas de gestión de bases de datos de código abierto más populares, y todos los ejemplos de esta lección funcionan con los dos, porque son compatibles. MariaDB es el motor que trae XAMPP, y su instalación está explicada en XAMPP, en el anexo.
JDBC: cómo habla Java con la base
Los programas en Java se comunican con las bases de datos y manipulan sus datos usando JDBC (Java DataBase Connectivity): una biblioteca de clases que permite la conexión con bases que soporten SQL desde Java.
El objetivo de JDBC es doble, y las dos partes son igual de importantes:
- Que la aplicación sea independiente de la plataforma.
- Que se pueda mover de un gestor a otro —de Oracle a MySQL, o a SQL Server— sin reescribir el programa.
Lo que hace posible esa independencia es el controlador (driver): un componente que permite a las aplicaciones Java conectarse a la base de un DBMS determinado y manipularla a través de la API de JDBC. La mayoría de los gestores populares incluyen su controlador. En esta lección se usa el de MySQL/MariaDB, y las mismas técnicas sirven para cualquier base relacional que tenga un controlador de JDBC.
Connection, Statement, ResultSet— y cada gestor aporta su implementación en un archivo aparte. Cambiar de MySQL a PostgreSQL no cambia una línea del código que consulta: cambia una cadena de texto con el nombre del controlador. Eso es programar hacia una interfaz y no hacia una implementación, el principio que se vio en la lección 5.
Crear la base de datos
Para crear una base relacional desde el administrador de MySQL o MariaDB conviene instalar alguna utilidad con interfaz web gráfica, y la más usada es phpMyAdmin.
phpMyAdmin es una aplicación web que sirve para interactuar con una base de datos de forma sencilla: se pueden crear bases y tablas, borrar o modificar datos, agregar registros, hacer copias de seguridad. Es tan útil que casi todos los servicios de hosting la incluyen. Además se la usa para crear los usuarios con los que el programa se va a conectar, que es la forma de darle acceso sin repartir la clave de administrador. Como es una aplicación escrita en PHP, necesita un servidor web Apache y el motor MySQL o MariaDB para funcionar.
Para no tener que instalar y configurar esas tres cosas por separado existen paquetes que lo hacen de una vez:
- XAMPP: independiente de la plataforma y de software libre. Trae MariaDB, el servidor web Apache y los intérpretes de PHP y Perl. Es el que se usa acá, y su instalación paso a paso está en XAMPP, en el anexo.
- WAMP: para Windows. Permite crear aplicaciones web y ejecutarlas con Apache, PHP y MySQL, e incluye phpMyAdmin.
- EasyPHP: instala en un solo paso el servidor Apache, el módulo de PHP y MySQL.
Con cualquiera de los tres queda disponible phpMyAdmin, que abre una página web desde la cual se crea la base con sus tablas, se agregan registros, se crean cuentas de usuario con sus privilegios y se hacen las copias de seguridad. Las bases que se usan en los ejemplos de esta lección se llaman instituto y tienen una tabla profesores y otra personas.
Conectarse y consultar
Primero: conseguir el controlador
Antes de conectarse desde un programa hay que tener cargado el conector JDBC correspondiente. Si no se trabaja con Maven, hay que bajar el connector/j y copiarlo en la carpeta \jre\lib\ext de la instalación de Java. Las páginas de descarga son las oficiales de cada proyecto: downloads.mariadb.org/connector-java para MariaDB y dev.mysql.com/downloads/connector/j para MySQL.
Si se trabaja con Maven, alcanza con agregar la dependencia en el pom.xml, que es el archivo donde se declaran las bibliotecas que usa el proyecto:
<!-- Para MariaDB -->
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.7.2</version>
</dependency>
<!-- Para MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
Los tres objetos de toda consulta
Para conectarse y consultar hay que definir tres objetos, que aparecen en todos los programas que usan una base de datos:
| Objeto | Para qué sirve |
|---|---|
Connection conexion | Maneja la conexión con la base de datos. |
Statement instruccion | Es la instrucción de consulta que se le va a enviar. |
ResultSet conjuntoResultados | Maneja los resultados que devuelve la base. |
Las cinco tareas
-
Cargar el controlador.
Class.forName(CONTROLADOR);CONTROLADORes una variable con el nombre del controlador del gestor que se va a usar. Es una cadena, y cambiar de gestor significa cambiar esa cadena:Gestor Controlador MySQL com.mysql.cj.jdbc.DriverMariaDB org.mariadb.jdbc.DriverPostgreSQL org.postgresql.DriverOracle oracle.jdbc.driver.OracleDriverDB2 COM.ibm.db2.jdbc.app.DB2DriverMS SQL Server com.microsoft.sqlserver.jdbc.SQLServerDriverSybase com.sybase.jdbc2.jdbc.SybDriverJava DB / Apache Derby org.apache.derby.jdbc.EmbeddedDriver -
Establecer la conexión. Se indica la URL de la base, el usuario y la contraseña:
La URL tiene un formato distinto en cada gestor:conexion = DriverManager.getConnection(URL_BASEDATOS, "usuario", "contraseña");Gestor Formato de la URL MySQL jdbc:mysql://host:puerto/baseMariaDB jdbc:mariadb://host:puerto/basePostgreSQL jdbc:postgresql://host:puerto/baseOracle jdbc:oracle:thin:@host:puerto:baseDB2 jdbc:db2:host:puerto/baseMS SQL Server jdbc:sqlserver://host:puerto;databaseName=baseSybase jdbc:sybase:Tds:host:puerto/baseJava DB / Apache Derby jdbc:derby:base(incrustado) ojdbc:derby://host:puerto/base(red) - Crear el objeto Statement para consultar:
instruccion = conexion.createStatement(); -
Ejecutar la consulta. Hay dos métodos, y cuál corresponde depende de qué hace la instrucción:
executeQuery()cuando la instrucción devuelve datos —unSELECT—. Devuelve unResultSet.executeUpdate()cuando la instrucción modifica datos —INSERT,UPDATE,DELETE—. Devuelve un número entero: la cantidad de filas afectadas.
- Mostrar los resultados, recorriendo el
ResultSetfila por fila.
El ejemplo completo
Este programa se conecta a la base instituto, consulta los profesores y los muestra en pantalla. Están las cinco tareas y, además, dos cosas que se ven por primera vez: el bloque finally que cierra los tres objetos, y los metadatos para que el encabezado de la tabla salga de la propia base y no esté escrito a mano.
import java.sql.Connection;
import java.sql.Statement;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
public class MostrarProfesores {
// nombre del controlador de JDBC y URL de la base de datos
static final String CONTROLADOR = "com.mysql.cj.jdbc.Driver";
static final String URL_BASEDATOS = "jdbc:mysql://localhost/instituto";
public static void main(String args[]) {
Connection conexion = null; // maneja la conexión
Statement instruccion = null; // instrucción de consulta
ResultSet conjuntoResultados = null; // maneja los resultados
try {
// 1) carga la clase controlador
Class.forName(CONTROLADOR);
// 2) establece la conexión (usuario por defecto sin contraseña)
conexion = DriverManager.getConnection(URL_BASEDATOS, "root", "");
// 3) crea el objeto Statement
instruccion = conexion.createStatement();
// 4) ejecuta la consulta
conjuntoResultados = instruccion.executeQuery(
"SELECT IDProfesor, Apellido, Nombre FROM profesores");
// 5) procesa los resultados
ResultSetMetaData metaDatos = conjuntoResultados.getMetaData();
int numeroDeColumnas = metaDatos.getColumnCount();
System.out.println("Tabla Profesores de la base de datos Instituto:\n");
// el encabezado sale de la base, no está escrito a mano
for (int i = 1; i <= numeroDeColumnas; i++) {
System.out.printf("%-8s\t", metaDatos.getColumnName(i));
}
System.out.println();
while (conjuntoResultados.next()) {
for (int i = 1; i <= numeroDeColumnas; i++) {
System.out.printf("%-8s\t", conjuntoResultados.getObject(i));
}
System.out.println();
}
}
catch (SQLException excepcionSql) {
excepcionSql.printStackTrace();
}
catch (ClassNotFoundException noEncontroClase) {
noEncontroClase.printStackTrace();
}
finally {
// asegura que los tres objetos queden cerrados
try {
conjuntoResultados.close();
instruccion.close();
conexion.close();
}
catch (Exception excepcion) {
excepcion.printStackTrace();
}
}
}
}
Cuatro detalles que hay que mirar de ese código. El finally cierra los tres objetos: es exactamente para lo que se vio en la lección 4, porque una conexión abierta ocupa un recurso del servidor y hay un límite. conjuntoResultados.next() avanza a la siguiente fila y devuelve false cuando no hay más: es la condición de corte del while. Los índices del ResultSet empiezan en 1, no en 0 —a diferencia de los vectores—, por eso los bucles van de 1 a numeroDeColumnas. Y por último, las dos excepciones verificadas: SQLException y ClassNotFoundException, las dos de la lección 4, que el compilador obliga a tratar. Si la base estuviera en otra computadora, se reemplaza localhost por su dirección IP.
Una ventana para consultar
Mostrar los datos por consola sirve para probar, pero lo que hace falta es una aplicación. El ejemplo del apunte es una ventana con un JTable que muestra el resultado de una consulta, y un área de texto donde el usuario puede escribir otra y ver el resultado al instante.
La arquitectura que se usa es el MVC (Modelo Vista Controlador), que es la aplicación concreta de SRP y DIP de la lección 5 a una interfaz gráfica. Las piezas son dos clases:
VentanaResultadosConsulta(la Vista): la ventana. Tiene elJTableque muestra los datos, elJTextAreadonde se escribe la consulta, el botón y el manejo de eventos. No sabe nada de SQL: le pasa el texto de la consulta a la otra clase.ResultSetTableModel(el Controlador): una clase que hereda deAbstractTableModel, que es lo que Swing necesita para llenar una tabla. Adentro tiene la conexión, la instrucción, elResultSety los metadatos, y traduce las preguntas que le hace la tabla —cuántas filas, cuántas columnas, qué hay en la celda tal— en consultas a la base.
La separación no es capricho. El JTable de Swing no muestra datos: pide datos. Cada vez que necesita dibujar una celda, le pregunta al modelo getValueAt(fila, columna), y el modelo va a buscar ese dato. Por eso alcanza con implementar cuatro métodos: getRowCount(), getColumnCount(), getColumnName() y getValueAt(). Y ahí hay un detalle que aparece siempre y conviene tener anotado:
ResultSet se cuentan desde 1, y las del JTable desde 0. Así que al traducir hay que sumarle 1 al índice que viene de la tabla: la columna 0 del JTable es la columna 1 del ResultSet, y la fila 0 del JTable es la fila 1 del ResultSet. Ese + 1 está en todas las consultas del modelo y es una fuente de errores silenciosos si se lo olvida.
El botón de la ventana ejecuta modeloTabla.establecerConsulta(areaConsulta.getText()), y si la consulta está mal escrita la base tira una SQLException que se muestra en un cuadro de diálogo. El programa además se recupera solo: cuando la consulta falla, vuelve a ejecutar la consulta predeterminada y restablece el texto, para que el usuario no se quede con una ventana rota. Es un buen ejemplo de lo que se decía en la lección 4 sobre no dar por hecho que lo que llega de afuera es válido.
Altas, bajas, modificaciones y búsqueda
Consultar es la mitad del trabajo. La otra mitad son las cuatro operaciones con las que se mantiene una tabla —altas, bajas, modificaciones y búsqueda, o ABMC—, cada una desde un formulario. El ejemplo del apunte es una ventana con los datos de un empleado en campos de texto y cinco botones: Anterior, Siguiente, Ver todos, Buscar un registro por ID, Insertar, Eliminar, Modificar, y los botones Grabar que aparecen solo cuando se está editando.
Las piezas son tres:
Persona— la clase con los datos:idPersona,nombre,apellido,emailytelefono, con susgetyset. Es la misma clase de datos de la lección 3.ConsultasPersona— la clase que hace todas las operaciones contra la base:insertarPersona,modificarPersona,borrarPersona,buscarPersona,getPersonasyclose()para cerrar la conexión.VentanaEmpleados— la ventana: los campos, los botones y los manejadores de eventos que llaman a los métodos deConsultasPersona. Y una claseTestABMCcon unmainde una sola línea que abre la ventana.
La novedad técnica del ABMC es que ya no se usa Statement sino PreparedStatement. La diferencia es esta:
// Con Statement: se arma el texto de la consulta pegando pedazos
String sql = "INSERT INTO personas (nombre, apellido) VALUES ('" + nombre + "', '" + apellido + "')";
// Con PreparedStatement: la instrucción tiene huecos y los valores van aparte
String sql = "INSERT INTO personas (nombre, apellido) VALUES (?, ?)";
El método executeUpdate() de un PreparedStatement devuelve un entero: la cantidad de filas afectadas. Por eso los métodos de ConsultasPersona devuelven int, y la ventana puede decir con precisión si la operación se hizo o no: si el resultado es 1, se modificó un registro.
PreparedStatement, y no es una cuestión de estilo. Armar la consulta pegando el texto que escribió el usuario es la puerta de entrada del ataque de inyección de SQL: si alguien escribe en el campo del nombre algo como '; DROP TABLE personas;--, ese texto pasa a formar parte de la instrucción y la base lo ejecuta. Con PreparedStatement los valores viajan aparte de la instrucción, así que la base los trata siempre como datos y nunca como código. Se escribe casi igual y elimina el problema de raíz.
Metadatos
Hay casos en los que hace falta conocer la estructura de una base de datos: los nombres y el diseño de las tablas, los tipos de los campos. Los datos que describen la estructura de la base son los metadatos. Y son datos que se pueden consultar desde el programa, sin saberlos de antemano.
Los metadatos de la base: DatabaseMetaData
Se obtienen con el método getMetaData() de la clase Connection, así que es el objeto de la conexión el que los da:
DatabaseMetaData metadatos = conexion.getMetaData();
| Método | Qué devuelve |
|---|---|
getColumns(catálogo, esquema, tabla, campo) | Un ResultSet donde cada fila es un campo que cumple el nombre indicado —que puede ser null—. |
getConnection() | El objeto Connection relacionado. |
getDatabaseProductName() | El nombre comercial del gestor en uso. |
getDatabaseProductVersion() | La versión del gestor en uso. |
getDriverName() | El nombre del driver JDBC en uso. |
getIndexInfo(catalog, schema, tabla, única, aproximación) | Un conjunto de resultados que describe los índices de la tabla indicada. |
getURL() | La URL del gestor, como cadena. |
getUserName() | El nombre del usuario actual del gestor. |
Estos métodos son los que permiten que un programa se adapte a distintas bases sin tener que configurarlo a mano: puede preguntarle a la base cómo está armada.
Los metadatos del resultado: ResultSetMetaData
El método getMetaData() de la clase ResultSet devuelve un objeto ResultSetMetaData, que da información sobre el conjunto de resultados: es lo que se usó en el ejemplo de la conexión para armar el encabezado de la tabla. Sus métodos más útiles:
| Método | Qué devuelve |
|---|---|
getColumnCount() | El número de columnas del conjunto de resultados. |
getColumnDisplaySize(n) | El ancho destinado en pantalla para mostrar el contenido de esa columna. |
getColumnName(n) | El nombre de la columna indicada. |
getColumnType(n) | El tipo de datos SQL estándar de la columna. |
getColumnTypeName(n) | El tipo de datos propio del gestor en uso para esa columna. |
getPrecision(n) | La precisión de decimales de la columna. |
getScale(n) | La cantidad de decimales que se muestran. |
isAutoincrement(n) | Si el campo es autoincrementable. |
isCaseSensitive(n) | Si la columna distingue mayúsculas de minúsculas. |
isReadOnly(n) | Si el campo es de solo lectura. |
isSearchable(n) | Si la columna puede aparecer en el WHERE de un SELECT. |
isSigned(n) | Si la columna admite números con signo. |
Un uso práctico de todo esto: un programa que genera un listado sin saber qué tabla va a mostrar. Pregunta el nombre y el tipo de cada columna, y arma solo la tabla en pantalla. La estructura de la base viaja con el resultado.
Procedimientos almacenados
Muchos gestores pueden guardar instrucciones SQL —o conjuntos de ellas— dentro de la base, para que los programas que tengan acceso puedan invocarlas. A esas instrucciones se las conoce como procedimientos almacenados (stored procedures).
JDBC permite invocarlos usando objetos que implementen la interfaz CallableStatement. Esos objetos:
- Pueden recibir argumentos con los métodos heredados de
PreparedStatement, o sea con los mismos huecos y valores separados que se acaban de ver. - Pueden tener parámetros de salida, que es lo que los distingue: un procedimiento almacenado puede devolver valores en ellos.
- La interfaz incluye métodos para indicar cuáles parámetros son de salida y para obtener sus valores después de ejecutar el procedimiento.
Vale la pena entender por qué se usan, más allá de la sintaxis: un procedimiento almacenado pone lógica del sistema del lado del servidor de base de datos, donde se ejecuta sin que los datos viajen por la red hasta el programa y vuelvan. Es la verificación de stock de un depósito, el cálculo de un total que tiene que ser exacto. El costo es que esa lógica queda escrita en SQL —y muchas veces en el dialecto de ese gestor— y no en Java, con lo cual se pierde parte de la portabilidad que da JDBC.
Transacciones
Muchas aplicaciones necesitan la garantía de que una serie de operaciones se ejecuten todas O ninguna, antes de seguir procesando la operación siguiente. El ejemplo del apunte es el mejor posible, porque cualquiera lo entiende:
Al transferir dinero entre dos cuentas bancarias, se especifica la cuenta de origen y el monto. Después, la cuenta de destino. El banco comprueba si hay fondos suficientes en la cuenta de origen; de ser así, retira el monto de la cuenta de origen y, si todo sale bien, deposita el dinero en la de destino.
¿Qué pasa si la transferencia falla después de que el banco retiró el dinero? En un sistema bancario apropiado, el banco vuelve a depositar el dinero en la cuenta de origen. ¿Cómo se sentiría uno si el dinero se restara de la cuenta de origen y no se depositara en la de destino?
El procesamiento de transacciones permite que un programa trate una operación —o un conjunto de ellas— como una sola operación. Se la llama también operación atómica. Al final de una transacción se puede tomar una de dos decisiones:
- Confirmar (
commit): se finalizan las operaciones. Todas las inserciones, actualizaciones y eliminaciones que se hicieron como parte de la transacción ya no se pueden invertir sin hacer una operación nueva. - Rechazar (
rollback): la base queda en el estado anterior a la operación. Es lo que corresponde cuando una parte de la transacción no se completó bien.
Java ofrece el procesamiento de transacciones a través de métodos de la interfaz Connection:
conexion.setAutoCommit(false); // a partir de acá, las instrucciones se agrupan
// ... las instrucciones SQL de la transacción ...
conexion.commit(); // si todo salió bien
// o
conexion.rollback(); // si algo falló: se vuelve atrás
El método setAutoCommit() es el que define el modo: con true, cada instrucción SQL se confirma sola al completarse —que es el comportamiento por defecto—; con false, las instrucciones se agrupan y forman una transacción. Cuando está en false, el programa está obligado a llamar a commit() o a rollback() después de la última instrucción. También existe getAutoCommit() para consultar en qué modo está la conexión.
Proceso por lotes
Una mejora importante de JDBC 2 fue la posibilidad de procesar múltiples instrucciones SQL mediante lotes (Batch). Los procesos por lotes permiten recopilar y lanzar de una vez una serie larga de instrucciones.
Se hace con tres métodos de la clase Statement:
addBatch()— agrega una instrucción nueva al lote.executeBatch()— lanza todas las instrucciones almacenadas.clearBatch()— borra el contenido del lote.
Y hay un detalle sobre el resultado que conviene entender bien: el resultado de executeBatch() es un arreglo de enteros, donde cada elemento es la cantidad de filas modificadas por la acción correspondiente. Si el primer comando del lote modificó tres filas y el segundo seis, el arreglo tiene dos elementos: el primero vale 3 y el segundo 6. Es la misma información que devuelve executeUpdate(), pero una vez por cada instrucción.
Una limitación importante: solo se pueden poner instrucciones de actualización —UPDATE, INSERT, CREATE TABLE, DELETE—. No sirve para consultar, porque el objetivo es reducir los viajes entre el programa y el servidor: en lugar de mandar mil INSERT y esperar mil respuestas, se manda uno solo con mil instrucciones.
Ejercicios
1. Complete el camino
Ordenar los pasos de una consulta a una base de datos desde Java: recorrer el ResultSet, obtener la Connection, cargar el controlador, ejecutar la consulta, crear el Statement, cerrar los objetos.
2. Cambiar de gestor
Se tiene un programa que se conecta a MySQL y hay que pasarlo a MariaDB. Indicar exactamente qué dos líneas del código cambian y qué dos archivos hay que tocar si se trabaja en lugar de con el driver suelto con Maven.
3. Statement o PreparedStatement
Explicar qué diferencia hay entre los dos y por qué uno de los dos es obligatorio cuando el valor lo escribe el usuario. Dar un ejemplo del problema que evita.
4. Los índices
Se consulta la tabla profesores con las columnas id, apellido y nombre, y se muestran en un JTable. Escribir el método getValueAt(fila, columna) del modelo, y explicar por qué hay que sumar 1.
5. Transacciones
Escribir el código de un método que transfiera un importe de una cuenta a otra, con las dos actualizaciones dentro de una transacción, y que rechace la operación si el saldo de la cuenta de origen no alcanza.
6. Elegir las herramientas
Indicar qué mecanismo conviene en cada caso y por qué:
- Insertar 5000 registros leídos de un archivo de texto.
- Cerrar una factura: insertar la factura, sus ítems y actualizar el stock.
- Mostrar en pantalla una tabla que el usuario eligió de una lista, sin saber sus columnas.
- Calcular el total de una factura dentro de la base, sin traer los ítems al programa.
Respuestas sugeridas
1. El orden es: cargar el controlador (Class.forName) → obtener la Connection (DriverManager.getConnection) → crear el Statement (conexion.createStatement()) → ejecutar la consulta (executeQuery) → recorrer el ResultSet con while (rs.next()) → cerrar los objetos en el finally. El último paso es el que más se olvida y el que más problemas trae: una conexión abierta ocupa un recurso del servidor, y los servidores tienen un límite.
2. Cambian dos líneas: la cadena del controlador —com.mysql.cj.jdbc.Driver pasa a org.mariadb.jdbc.Driver— y la URL —jdbc:mysql://localhost/instituto pasa a jdbc:mariadb://localhost/instituto—. Y con Maven hay que cambiar la dependencia del pom.xml por org.mariadb.jdbc:mariadb-java-client. Si en cambio se usa el driver suelto, hay que reemplazar el archivo .jar en \jre\lib\ext. Ni una línea del código que consulta cambia: esa es exactamente la ventaja de JDBC.
3. Con Statement la instrucción se arma como un texto completo, pegando los valores con concatenación. Con PreparedStatement la instrucción tiene huecos (?) y los valores se cargan aparte. El segundo es obligatorio cuando el valor lo escribe el usuario porque evita la inyección de SQL: si alguien escribe en un campo '; DROP TABLE personas;-- y ese texto se concatena, la base lo interpreta como una instrucción y borra la tabla. Con PreparedStatement ese texto viaja como dato y la base lo guarda como texto. Ventaja extra: como la instrucción siempre es la misma, el gestor la puede tener preparada y es más rápido ejecutarla muchas veces.
4.
public Object getValueAt(int fila, int columna) throws IllegalStateException {
if (!conectadoABaseDatos) {
throw new IllegalStateException("No hay conexion a la Base de Datos");
}
try {
conjuntoResultados.absolute(fila + 1);
return conjuntoResultados.getObject(columna + 1);
} catch (SQLException excepcionSql) {
excepcionSql.printStackTrace();
}
return "";
}
Hay que sumar 1 porque el JTable cuenta las filas y las columnas desde 0 y el ResultSet desde 1. El absolute(fila + 1) es necesario porque el ResultSet es secuencial: para leer una celda cualquiera hay que ubicarse primero en su fila. Y el + 1 del final del método sí es una convención de la tabla: los números de fila del JTable también empiezan en 1 para el usuario.
5.
public boolean transferir(int idOrigen, int idDestino, double importe) {
try {
conexion.setAutoCommit(false);
// 1) verificar que haya fondos
PreparedStatement consulta = conexion.prepareStatement(
"SELECT saldo FROM cuentas WHERE id = ?");
consulta.setInt(1, idOrigen);
ResultSet rs = consulta.executeQuery();
if (!rs.next() || rs.getDouble("saldo") < importe) {
conexion.rollback(); // no hay fondos: se vuelve atrás
return false;
}
// 2) retirar de la cuenta de origen
PreparedStatement retiro = conexion.prepareStatement(
"UPDATE cuentas SET saldo = saldo - ? WHERE id = ?");
retiro.setDouble(1, importe);
retiro.setInt(2, idOrigen);
retiro.executeUpdate();
// 3) depositar en la cuenta de destino
PreparedStatement deposito = conexion.prepareStatement(
"UPDATE cuentas SET saldo = saldo + ? WHERE id = ?");
deposito.setDouble(1, importe);
deposito.setInt(2, idDestino);
deposito.executeUpdate();
conexion.commit(); // las dos operaciones se confirman juntas
return true;
}
catch (SQLException e) {
try {
conexion.rollback(); // si algo falló, no se hizo nada
} catch (SQLException ex) {
ex.printStackTrace();
}
return false;
}
}
Lo importante del ejercicio es dónde está el rollback(): en los dos caminos de error. Si no hay fondos, se vuelve atrás antes de cerrar; y si algo falla en el medio de las dos actualizaciones, el catch se encarga. Sin eso, el retiro podría haberse aplicado y el depósito no.
6. (a) Proceso por lotes: agrupar los 5000 INSERT con addBatch() y lanzarlos con executeBatch(). Son 5000 viajes de red menos. (b) Transacción: las tres operaciones tienen que aplicarse juntas o ninguna; una factura sin sus ítems, o con el stock descontado dos veces, es un error grave. (c) Metadatos (ResultSetMetaData): se le pregunta al resultado cuántas columnas tiene y cómo se llaman, y con eso se arma la tabla sin saber nada de antemano. (d) Procedimiento almacenado: el cálculo se hace del lado del servidor y solo viaja el total, en lugar de traer todos los ítems y sumarlos en Java.
Resumen
- Una base de datos guarda datos de forma estructurada y con poca redundancia, y permite que varios usuarios y programas accedan a la vez, con permisos distintos. Un gestor (DBMS o SGBD) es el software que lo administra.
- Los datos se pueden organizar según distintos modelos: jerárquico, de red, relacional, deductivo y orientado a objetos. El relacional es el que se usa casi siempre: tablas de filas y columnas, relacionadas por claves, con SQL como lenguaje.
- JDBC es la biblioteca que permite a Java conectarse a cualquier base con SQL. La independencia la da el controlador de cada gestor, y para usarlo hay que cargarlo con
Class.forName()o declararlo como dependencia de Maven. - Toda consulta usa tres objetos:
Connection,StatementyResultSet. Los pasos son: cargar el controlador, conectarse, crear el statement, ejecutar —executeQuerypara leer,executeUpdatepara modificar— y recorrer el resultado connext(). Los índices del ResultSet empiezan en 1. - La conexión y los objetos se cierran siempre en el
finally, y las excepcionesSQLExceptionyClassNotFoundExceptionhay que tratar. - Una aplicación con interfaz se organiza en MVC: la ventana muestra, el modelo de tabla —que hereda de
AbstractTableModel— consulta. ElJTablepide los datos celda por celda, y hay que traducir sus índices desde 0 a los delResultSetdesde 1. - Para las altas, bajas, modificaciones y búsquedas se usa
PreparedStatement, que además de ser más rápido evita la inyección de SQL: los valores viajan separados de la instrucción.executeUpdate()devuelve la cantidad de filas afectadas. - Los metadatos describen la estructura:
DatabaseMetaDatada información de la base —nombre y versión del gestor, columnas, índices— yResultSetMetaDatadel resultado —cuántas columnas, cómo se llaman, de qué tipo—. - Los procedimientos almacenados son instrucciones SQL guardadas dentro de la base y se invocan con
CallableStatement, que permite parámetros de salida. Sirven para que la lógica quede del lado del servidor. - Una transacción trata varias operaciones como una sola, atómica: se confirma con
commit()o se rechaza conrollback(). Se activa consetAutoCommit(false), y es imprescindible en operaciones como una transferencia bancaria. - El proceso por lotes agrupa muchas instrucciones de actualización con
addBatch()y las lanza conexecuteBatch(), que devuelve un arreglo con la cantidad de filas afectadas por cada una.
Con esto el programa guarda sus datos donde corresponde. Lo que falta es dejar de escribir SQL a mano: eso es la lección 8.
Licencia. Obra derivada de los apuntes de clase del Prof. Matías E. García —Clase Teórica 6—, publicada en profmatiasgarcia.com.ar bajo CC BY-NC-SA 2.5 Argentina; el desarrollo del texto, los diagramas y los ejercicios son de Prof Mauro Bobyk.