Acceso a
Bases de Datos con Java: JDBC (I) por Pedro Agulló Soliveres Publicado en Revista Profesional para Programadores (RPP) |
JDBC es la API estándar para acceso a Bases de Datos en aplicaciones Java. En este artículo estudiaremos cuáles son las principales características de JDBC, y comenzaremos a abordar las clases e interfaces que proporciona esta API. |
JDBC es la API estándar de acceso a Bases de Datos con Java, y se incluye con el Kit de Desarrollo de Java (JDK) a partir de la versión 1.1. Sun optó por crear una nueva API, en lugar de utilizar APIs ya existentes, como ODBC, con la intención de obviar los problemas que presenta el uso desde Java de estas APIs, que suelen ser de muy bajo nivel y utilizar características no soportadas directamente por Java, como punteros, etc. Aunque el nivel de abstracción al que trabaja JDBC es alto en comparación, por ejemplo, con ODBC, la intención de Sun es que sea la base de partida para crear librerías de más alto nivel, en las que incluso el hecho de que se use SQL para acceder a la información sea invisible.
Para trabajar con JDBC es necesario tener controladores (drivers) que permitan acceder a las distintas Bases de Datos: cada vez hay más controladores nativos JDBC. Sin embargo, ODBC es hoy en día la API más popular para acceso a Bases de Datos: Sun admite este hecho, por lo que, en colaboración con Intersolv (uno de principales proveedores de drivers ODBC) ha diseñado un puente que permite utilizar la API de JDBC en combinación con controladores ODBC. En este artículo se utilizará este puente JDBC/ODBC para acceder a una Base de Datos creada con Interbase (SGBD para el que también existe un driver JDBC nativo, InterClient).
Un último detalle: algunos fabricantes, como Microsoft, ofrecen sus propias APIs, en lugar de JDBC, como RDO, etc. Aunque estas APIs pueden ser muy eficientes, y perfectamente utilizables con Java, su uso requiere tener muy claras las consecuencias, sobre todo la pérdida de portabilidad, factor decisivo en la mayor parte de los casos para escoger Java en lugar de otro lenguaje: ¿existe alguna implementación de RDO para Java bajo OS/2, por ejemplo?.
Antes de comenzar a estudiar las distintas clases proporcionadas por JDBC, vamos a abordar la creación de una fuente de datos ODBC, a través de la cuál accederemos a una Base de Datos Interbase. Hemos escogido este modo de acceso en lugar de utilizar un driver JDBC nativo para Interbase porque ello nos permitirá mostrar el uso de drivers ODBC: de este modo, aquél que no disponga de Interbase, podrá crear una Base de Datos Access, BTrieve o de cualquier otro tipo para el que sí tenga un driver ODBC instalado en su sistema, y utilizar el código fuente incluido con el diskette que acompaña a la revista.
El Listado A muestra el SQL utilizado para crear nuestra pequeña Base de Datos de ejemplo, incluida en el diskette. Nótese que el campo ID de la tabla CONTACTOS es del tipo contador o autoincremento, y requiere de la definición de un trigger y un contador en Interbase: otros sistemas de Base de Datos pueden tener soporte directo para este tipo de campo, lo que hará innecesario parte del código SQL del Listado A.
/* No se hace commit automatico de sentencias de definicion de datos, DDL */ SET AUTODDL OFF; /********** Creacion de la base de datos **********/ /* Necesario USER y PASSWORD si no se ha hecho login previo */ CONNECT "h:\work\artículos\jdbc\1\fuente\agenda.gdb" USER "SYSDBA" PASSWORD "masterkey"; /* Eliminación de la Base de datos */ DROP DATABASE; CREATE DATABASE "h:\work\artículos\jdbc\1\fuente\agenda.gdb" USER "SYSDBA" PASSWORD "masterkey"; CONNECT "h:\work\artículos\jdbc\1\fuente\agenda.gdb" USER "SYSDBA" PASSWORD "masterkey"; /***** Creación de la tabla de Contactos *****/ CREATE TABLE CONTACTOS ( ID INTEGER NOT NULL, NOMBRE CHAR(30) NOT NULL, EMPRESA CHAR(30), CARGO CHAR(20), DIRECCION CHAR(40), NOTAS VARCHAR(150), PRIMARY KEY( ID ) ); CREATE INDEX I_NOMBRE ON CONTACTOS( NOMBRE ); /* Creamos un contador para CONTACTOS.ID */ CREATE GENERATOR CONTACTOS_ID_Gen; SET GENERATOR CONTACTOS_ID_Gen TO 0; /* Creamos trigger para insertar código del contador en CONTACTOS */ SET TERM !! ; CREATE TRIGGER Set_CONTACTOS_ID FOR CONTACTOS BEFORE INSERT AS BEGIN New.ID = GEN_ID( CONTACTOS_ID_Gen, 1); END !! SET TERM ; !! /**** Creación de la tabla de teléfonos de contactos ****/ CREATE TABLE TELEFONOS ( CONTACTO INTEGER NOT NULL, TELEFONO CHAR(14) NOT NULL, NOTAS VARCHAR(50), PRIMARY KEY( CONTACTO, TELEFONO ) ); /* Se hace COMMIT */ EXIT; |
El primer paso, evidentemente, será crear la Base de Datos, que en nuestro caso se llama AGENDA.GDB. Una vez hecho esto, debemos crear una fuente de datos ODBC: para ello, se debe escoger en el Panel de Control el icono "32bit ODBC", correspondiente al gestor de fuentes de datos ODBC, y hacer click sobre el botón "Add..." de la página "User DSN", con lo que aparecerá la ventana de la Figura A.
Hecho esto, queda configurar la fuente de datos, indicando el nombre que le vamos a dar (que utilizaremos a la hora de conectarnos a la Base de Datos), así como otros datos. La Figura B muestra la ventana de configuración de una fuente de datos Interbase, utilizando un driver de Visigenic.
Data Source Name es el nombre que utilizaremos para identificar la fuente de datos, y en nuestro caso será JDBC_AGENDA. Description es un texto explicativo optativo, y Database especifica cómo se llama y dónde se encuentra la Base de Datos que vamos a acceder a través de esta fuente de datos. Para poder utilizar los programas de ejemplo, deberemos introducir aquí el directorio donde copiemos los ejemplos, seguido del nombre de la Base de Datos, AGENDA.GDB. En mi caso, Database es h:\work\artículos\jdbc\1\fuente\AGENDA.GDB.
El hecho de que para acceder una Base de Datos mediante ODBC se utilice el nombre de la fuente de datos, en lugar del nombre de la Base de Datos, nos permite cambiar la ubicación e incluso el nombre de la misma sin tener que modificar el código fuente de nuestros programas, ya que estos utilizarán el nombre de la fuente de datos para identificarla.
En cuanto a los demás parámetros de la fuente de datos, se debe escoger como Network Protocol la opción <local>. El nombre de usuario (User name) y la contraseña (Password), se indicarán manualmente desde dentro de nuestro programa: no suele ser aconsejable especificarlo en la misma fuente de datos, dado que esto daría acceso a la Base de Datos a cualquier programa que acceda a la misma a través de la misma.
Como último paso, no debemos olvidar arrancar el servidor de Base de Datos, si utilizamos un gestor que incluye uno, como sucede con Interbase.
Como es lógico, la API JDBC incluye varias clases que se deben utilizar para conseguir acceso a una Base de Datos. La Tabla A muestra la lista de clases e interfaces más importantes que JDBC ofrece, junto con una breve descripción. Estas clases se encuentran en el paquete java.sql.
Clase/Interface |
Descripción |
Driver |
Permite conectarse a una Base de Datos: cada gestor de Base de Datos requiere un Driver distinto. |
DriverManager |
Permite gestionar todos los Drivers instalados en el sistema. |
DriverPropertyInfo |
Proporciona diversa información acerca de un Driver. |
Connection |
Representa una conexión con una Base de Datos. Una aplicación puede tener más de una conexión a más de una Base de Datos. |
DatabaseMetadata |
Proporciona información acerca de una Base de Datos, como las tablas que contiene, etc. |
Statement |
Permite ejecutar sentencias SQL sin parámetros. |
PreparedStatement |
Permite ejecutar sentencias SQL con parámetros de entrada. |
CallableStatement |
Permite ejecutar sentencias SQL con parámetros de entrada y salida, típicamente procedimientos almacenados. |
ResultSet |
Contiene las filas o registros obtenidos al ejecutar un SELECT. |
ResultSetMetadata |
Permite obtener información sobre un ResultSet, como el número de columnas, sus nombres, etc. |
El mejor modo de comenzar a estudiar la API de JDBC es empezar con un pequeño programa. El Listado B corresponde a un programa que carga el driver utilizado para conectarnos a bases de datos utilizando controladores ODBC.
// Importamos el paquete que da soporte a JDBC, java.sql import java.sql.*; import java.io.*; class jdbc1 { public static void main( String[] args ) { try { // Accederemos a la fuente de datos // ODBC llamada JDBC_AGENDA String urlBD = "jdbc:odbc:JDBC_AGENDA"; String usuarioBD = "SYSDBA"; String passwordBD = "masterkey"; // Cargamos la clase que implementa // el puente JDBC=>ODBC Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" ); // Establecemos una conexion con la Base de Datos System.out.println( "Estableciendo conexión con " + urlBD + "..." ); Connection conexion = DriverManager.getConnection( urlBD, usuarioBD, passwordBD ); System.out.println( "Conexión establecida." ); conexion.close(); System.out.println("Conexión a " + urlBD " cerrada."); } catch( Exception ex ) { System.out.println( "Se produjo un error." ); } } } |
Establecer una conexión con una Base de Datos mediante JDBC es sencillo: en primer lugar, registramos el Driver a utilizar (que en nuestro caso es el puente JDBC/ODBC), mediante el código
Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );
Si no se registra este driver, se producirá un error intentar la conexión. A continuación, se lleva a cabo la conexión a la Base de Datos mediante el código
Connection conexion = DriverManager.getConnection( urlBD, usuarioBD, passwordBD );
La clase DriverManager gestiona los Drivers registrados en el sistema: al llamar a getConnection, recorre la lista de Drivers cargados hasta encontrar uno que sea capaz de gestionar la petición especificada por urlBD, que en nuestro ejemplo es "jdbc:odbc:JDBC_AGENDA". Los parámetros usuarioBD y passwordBD corresponden al nombre del usuario y su contraseña, necesarios la mayor parte de las veces para acceder a cualquier Base de Datos.
Cómo se especifica la Base de Datos a utilizar depende del Driver utilizado: en nuestro caso, en que utilizamos el puente ODBC/JDBC, todas las peticiones serán de la forma "jdbc:odbc:NOMBRE_FUENTE_DATOS".
La cadena utilizada para indicar la Base de Datos siempre tiene tres partes, separadas por el carácter ":". La primera parte es siempre "jdbc". La segunda parte indica el subprotocolo, y depende del Sistema de Gestión de Base de Datos utilizado: en nuestro caso, es "odbc", pero para SQLAnyware, por ejemplo, es "dbaw". La tercera parte identifica la Base de Datos concreta a la que nos deseamos conectar, en nuestro caso la especificada por la fuente de datos ODBC llamada "JDBC_AGENDA". Aquí también se pueden incluir diversos parámetros necesarios para establecer la conexión, o cualquier otra información que el fabricante del SGBD indique.
Siguiendo con el Listado B, a continuación se establece una conexión con la Base de Datos, mediante el código
Connection conexion = controlador.connect( urlBD, usuarioBD, passwordBD );
Acto seguido cerramos la conexión, con
conexión.close();
Es conveniente cerrar las conexiones a Bases de Datos tan pronto como dejen de utilizarse, para liberar recursos rápidamente. Sin embargo, ha de tenerse en cuenta que establecer una conexión es una operación lenta, por lo que tampoco se debe estar abriendo y cerrando conexiones con frecuencia.
Un programa que realice una consulta y quiera mostrar el resultado de la misma requerirá del uso de varias clases: la priemra es DriverManager, que permitirá llevar a cabo una conexión con una Base de Datos, conexión que se representa mediante un objeto que soporta el interface Connection. También será necesario además ejecutar una sentencia SELECT para llevar a cabo la consulta, que se representará por un objeto que soporte el interface Statement (o PreparedStatement, o CallableStatement, que estudiaremos más adelante). Una sentencia SELECT puede devolver diversos registros o filas: esta información es accesible mediante un objeto que soporte el interface ResultSet.
El Listado C muestra el código fuente correspondiente a un pequeño programa que muestra los nombres de todos los contactos que hemos almacenado en la Base de Datos AGENDA.GDB, y que utiliza las clases anteriormente mencionadas.
import java.sql.*; import java.io.*; class jdbc2 { public static void main( String[] args ) { try { // Accederemos al alias ODBC llamado JDBC_DEMO String urlBD = "jdbc:odbc:JDBC_AGENDA"; String usuarioBD = "SYSDBA"; String passwordBD = "masterkey"; // Cargamos el puente JDBC=>ODBC Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver"); // Intentamos conectarnos a la base de datos JDBC_DEMO Connection conexion = DriverManager.getConnection ( urlBD, usuarioBD, passwordBD ); // Creamos una sentencia SQL Statement select = conexion.createStatement(); // Ejecutamos una sentencia SELECT ResultSet resultadoSelect = select.executeQuery( "SELECT * FROM CONTACTOS ORDER BY NOMBRE" ); // Imprimimos el nombre de cada contacto encontrado // por el SELECT System.out.println( "NOMBRE" ); System.out.println( "------" ); int col = resultadoSelect.findColumn( "NOMBRE" ); boolean seguir = resultadoSelect.next(); // Mientras queden registros... while( seguir ) { System.out.println( resultadoSelect.getString( col ) ); seguir = resultadoSelect.next(); }; // Liberamos recursos rápidamente resultadoSelect.close(); select.close(); conexion.close(); } catch( SQLException ex ) { // Mostramos toda la informaci¢n sobre // el error disponible System.out.println( "Error: SQLException" ); while (ex != null) { System.out.println ("SQLState: " + ex.getSQLState ()); System.out.println ("Mensaje: " + ex.getMessage ()); System.out.println ("Vendedor: " + ex.getErrorCode ()); ex = ex.getNextException(); System.out.println (""); } } catch( Exception ex ) { System.out.println( "Se produjo un error inesperado" ); } } } |
Echemos un vistazo al código en el Listado C. El código para registrar el controlador (Driver) a utilizar es el mismo del Listado B. A continuación se crea una sentencia SQL (Statement), y se ejecuta, obteniendo seguidamente el resultado de la misma (ResultSet), para lo que se utiliza el siguiente código:
// Creamos una sentencia SQL Statement select = conexion.createStatement(); // Ejecutamos una sentencia SELECT ResultSet resultadoSelect = select.executeQuery( "SELECT * FROM CONTACTOS ORDER BY NOMBRE" );
Además del método executeQuery, utilizado para ejecutar sentencias SELECT, Statement también proporciona otros métodos: executeUpdate ejecuta una sentencia UPDATE, DELETE, INSERT o cualquier otra sentencia SQL que no devuelva un conjunto de registros, y retorna el número de registros afectados por la sentencia (o -1 si no los hubo). El método getResultSet devuelve el ResultSet de la sentencia, si lo tiene, mientras que getUpdateCount devuelve el mismo valor que executeUpdate.
Es posible limitar el número máximo de registros devuelto al hacer un executeQuery mediante setMaxRows, y averiguar dicho número mediante getMaxRows. Es posible también obtener el último aviso generado al ejecutar una sentencia, mediante getWarning, así como limitar el tiempo en segundos que el controlador esperará hasta que el SGBD devuelva un resultado, mediante setQueryTimeout. Por último, el método close libera los recursos asociados a la sentencia.
El interface ResultSet es el que encapsula el resultado de una sentencia SELECT. Para recuperar la información es necesario acceder a las distintas columnas (o campos), y recuperarla mediante una serie de métodos getString, getFloat, getInt, etc. Al utilizar estos métodos debe indicarse el número correspondiente a la columna que estamos accediendo: si lo desconocemos, podemos averiguar el número correspondiente a una columna, dado el nombre, mediante findColumn. Por último, dado que un ResultSet puede contener más de un registro, para ir avanzando por la lista de registros que contiene deberemos utilizar el método next, que devuelve un valor booleano indicando si existe otro registro delante del actual. El uso de los métodos anteriores se ilustra en el Listado C, que recorre la lista de contactos en la Base de Datos, obteniendo el valor del campo (o columna) "NOMBRE", e imprimiéndolo, mediante un bucle en el que se llama repetidamente al método next y a getString.
Además de estos métodos, ResultSet también cuenta con getWarnings, que devuelve el primer aviso obtenido al manipular los registros, así como wasNull, que indica si el contenido de la última columna accedida es un NULL SQL. Por último, el método close libera los recursos asociados al ResultSet.
Excepción |
Descripción |
SQLException |
Error SQL. |
SQLWarning |
Advertencia SQL. |
DataTruncation |
Producida cuando se truncan datos inesperadamente, por ejemplo al intentar almacenar un texto demasiado largo en un campo. |
Al utilizar la API JDBC es posible obtener diversos errores debido a que se ha escrito incorrectamente una sentencia SQL, a que no se puede establecer una conexión con la Base de Datos por cualquier problema, etc.
El paquete java.sql proporciona tres nuevas excepciones, listadas en la Tabla A. En el Listado C se muestra un ejemplo de uso de las excepciones del tipo SQLException: es posible que se encadenen varias excepciones de este tipo, motivo por el que esta clase proporciona el método getNextException.
La excepción SQLWarning es silenciosa, no se suele elevar: para averiguar si la Base de Datos emitió un aviso se debe utilizar el método getWarnings de Statement y otras clases, que devuelve excepciones de esta clase. Dado que es posible que se obtengan varios avisos encadenados, esta clase proporciona el método getNextWarning, que devuelve el siguiente aviso, si lo hay.
Por último, las excepciones del tipo DataTruncation se producen cuando se trunca información inesperadamente, ya sea al leerla o al escribirla.
Este mes hemos estudiado las clases Driver, DriverManager, Connection, Statement y ResultSet. En el siguiente artículo abordaremos el uso de PreparedStatement y CallableStatement, derivadas de Statement, y estudiaremos en detalle la funcionalidad de Connection.
También abordaremos con mayor profundidad las clases DriverManager y Driver, y estudiaremos DriverPropertyInfo, lo que nos permitirá obtener información detallada sobre cada uno de los controladores de Base de Datos.
Por último, se verán ResultSetMetadata, que nos proporciona diversa información de interés sobre un ResultSet, y DatabaseMetadata, que nos permite averiguar información sobre una Base de Datos, como las tablas existente, etc.