Tutorial de Java

Aproximación a JDBC

Anterior | Siguiente

JDBC define ocho interfaces para operaciones con bases de datos, de las que se derivan las clases correspondientes. La figura siguiente, en formato OMT, con nomenclatura UML, muestra la interrelación entre estas clases según el modelo de objetos de la especificación de JDBC.

La clase que se encarga de cargar inicialmente todos los drivers JDBC disponibles es DriverManager. Una aplicación puede utilizar DriverManager para obtener un objeto de tipo conexión, Connection, con una base de datos. La conexión se especifica siguiendo una sintaxis basada en la especificación más amplia de los URL, de la forma

jdbc:subprotocolo//servidor:puerto/base de datos

Por ejemplo, si se utiliza mSQL el nombre del subprotocolo será msql. En algunas ocasiones es necesario identificar aún más el protocolo. Por ejemplo, si se usa el puente JDBC-ODBC no es suficiente con jdbc:odbc, ya que pueden existir múltiples drivers ODBC, y en este caso, hay que especificar aún más, mediante jdbc:odbc:fuente de datos.

Una vez que se tiene un objeto de tipo Connection, se pueden crear sentencias, statements, ejecutables. Cada una de estas sentencias puede devolver cero o más resultados, que se devuelven como objetos de tipo ResultSet.

Y la tabla siguiente muestra la misma lista de clases e interfaces junto con una breve descripción.

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/TD>
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.

La primera aplicación que se va a crear simplemente crea una tabla en el servidor, utilizando para ello el puente JDBC-ODBC, siendo la fuente de datos un servidor SQL Server. Si el lector desea utilizar otra fuente ODBC, no tiene más que cambiar los parámetros de getConnection() en el código fuente. El establecimiento de la conexión es, como se puede es fácil suponer, la parte que mayores problemas puede dar en una aplicación de este tipo. Si algo no funciona, cosa más que probable en los primeros intentos, es muy recomendable activar la traza de llamadas ODBC desde el panel de control. De esta forma se puede ver lo que está haciendo exactamente el driver JDBC y por qué motivo no se está estableciendo la conexión.

El siguiente diagrama relaciona las cuatro clases principales que va a usar cualquier programa Java con JDBC, y representa el esqueleto de cualquiera de los programas que se desarrollan para atacar a bases de datos.

La aplicación siguiente es un ejemplo en donde se aplica el esquema anterior, se trata de instalación java2101.java, crea una tabla y rellena algunos datos iniciales.

import java.sql.*;

class java2101 {
  static public void main( String[] args ) {
    Connection conexion;
    Statement sentencia;
    ResultSet resultado;

    System.out.println( "Iniciando programa." );

    // Se carga el driver JDBC-ODBC
    try {
      Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );
   } catch( Exception e ) {
      System.out.println( "No se pudo cargar el puente JDBC-ODBC." );
      return;
    }

    try {
      // Se establece la conexión con la base de datos
      conexion = DriverManager.getConnection( "jdbc:odbc:Tutorial","","" );
      sentencia = conexion.createStatement();
      try {
         // Se elimina la tabla en caso de que ya existiese
         sentencia.execute( "DROP TABLE AGENDA" );
      } catch( SQLException e ) {};

      // Esto es código SQL
      sentencia.execute( "CREATE TABLE AMIGOS ("+
        " NOMBRE VARCHAR(15) NOT NULL, " +
        " APELLIDOS VARCHAR(30) NOT NULL, " +
        " CUMPLE DATETIME) " );
      sentencia.execute( "INSERT INTO AMIGOS " +
        "VALUES('JOSE','GONZALEZ','03/15/1973')" );
      sentencia.execute( "INSERT INTO AMIGOS " +
        "VALUES('PEDRO','GOMEZ','08/15/1961')" );
      sentencia.execute( "INSERT INTO AMIGOS " +
        "VALUES('GONZALO','PEREZ', NULL)" );
    } catch( Exception e ) {
      System.out.println( e );
      return;
    }
  System.out.println( "Creacion finalizada." );
  } 
}

Las partes más interesantes del código son las que se van a revisar a continuación, profundizando en cada uno de los pasos.

Lo primero que se hace es importar toda la funcionalidad de JDBC, a través de la primera sentencia ejecutable del programa.

import java.sql.*;

Las siguientes líneas son las que cargan el puente JDBC-ODBC, mediante el método forName() de la clase Class.

try {
  Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );
  } catch( Exception e ) {
    System.out.println( "No se pudo cargar el puente JDBC-ODBC." );
    return;
  }

En teoría esto no es necesario, ya que DriverManager se encarga de leer todos los drivers JDBC compatibles, pero no siempre ocurre así, por lo que es mejor asegurarse. El método forName() localiza, lee y enlaza dinámicamente una clase determinada. Para drivers JDBC, la sintaxis que JavaSoft recomienda de forName() es nombreEmpresa.nombreBaseDatos.nombreDriver, y el driver deberá estar ubicado en el directorio nombreEmpresa\nombreBaseDatos\nombreDriver.class a partir del directorio indicado por la variable de entorno CLASSPATH. En este caso se indica que el puente JDBC-ODBC que se desea leer es precisamente el de Sun.

Si por cualquier motivo no es posible conseguir cargar JdbcOdbcDriver.class, se intercepta la excepción y se sale del programa. En este momento es la hora de echar mano de la información que puedan proporcionar las trazas ODBC.

La carga del driver también se puede especificar desde la línea de comandos al lanzar la aplicación:

java -Djdbc.drivers=sun.jdbc.odbc.JdbcOdbcDriver ElPrograma

A continuación, se solicita a DriverManager que proporcione una conexión para una fuente de datos ODBC. El parámetro jdbc:odbc:Tutorial especifica que la intención es acceder a la fuente de datos con nombre Tutorial, Data Source Name o DSN, en la terminología ODBC.

conexion = DriverManager.getConnection("jdbc:odbc:Tutorial","","" );

El segundo y tercer parámetro son el nombre del usuario y la clave con la cual se intentará la conexión. En este caso el acceso es libre, para acceder como administrador del sistema en el caso de un servidor MS SQL se usa la cuenta sa o system administrator, cuya cuenta de acceso no tiene clave definida; en caso de acceder a un servidor MS Access, la cuenta del administrador es admin y también sin clave definida. Esta es la única línea que con seguridad habrá de cambiar el programador para probar sus aplicaciones. getConnection admite también una forma con un único parámetro (el URL de la base de datos), que debe proporcionar toda la información de conexión necesaria al driver JDBC correspondiente. Para el caso JDBC-ODBC, se puede utilizar la sentencia equivalente:

DriverManager.getConnection ( "jdbc:odbc:SQL;UID=sa;PWD=" );

Para el resto de los drivers JDBC, habrá que consultar la documentación de cada driver en concreto.

Inmediatamente después de obtener la conexión, en la siguiente línea

sentencia = conexion.createStatement();

se solicita que proporcione un objeto de tipo Statement para poder ejecutar sentencias a través de esa conexión. Para ello se dispone de los métodos execute(String sentencia) para ejecutar una petición SQL que no devuelve datos o executeQuery(String sentencia) para ejecutar una consulta SQL. Este último método devuelve un objeto de tipo ResultSet.

Una vez que se tiene el objeto Statement ya se pueden lanzar consultas y ejecutar sentencias contra el servidor. A partir de aquí el resto del programa realmente es SQL «adornado»: en la línea:

sentencia.execute( "DROP TABLE AMIGOS" );

se ejecuta DROP TABLE AMIGOS para borrar cualquier tabla existente anteriormente. Puesto que este ejemplo es una aplicación «de instalación» y es posible que la tabla AMIGOS no exista, dando como resultado una excepción, se aísla la sentencia.execute() mediante un try y un catch.

La línea siguiente ejecuta una sentencia SQL que crea la tabla AMIGOS con tres campos: NOMBRE, APELLIDOS y CUMPLE. De ellos, únicamente el tercero, correspondiente al cumpleaños, es el que puede ser desconocido, es decir, puede contener valores nulos.

sentencia.execute( "CREATE TABLE AMIGOS ("+
        " NOMBRE VARCHAR(15) NOT NULL, " +
        " APELLIDOS VARCHAR(30) NOT NULL, " +
        " CUMPLE DATETIME) " );

Y ya en las líneas siguientes se ejecutan sentencias INSERT para rellenar con datos la tabla. En todo momento se ha colocado un try ... catch exterior para interceptar cualquier excepción que puedan dar las sentencias. En general, para java.sql está definida una clase especial de excepciones que es SQLException. Se obtendrá una excepción de este tipo cuando ocurra cualquier error de proceso de JDBC, tanto si es a nivel JDBC como si es a nivel inferior (ODBC o de protocolo).

Por ejemplo, si en lugar de GONZALO en la línea correspondiente a la última inserción en la Base de Datos, se intenta añadir un nombre nulo (NULL), se generará una excepción SQLException con el mensaje

[Microsoft][ODBC SQL Server Driver][SQL Server]Attempt to insert the value NULL into column 'NOMBRE', table 'master.dbo.AGENDA'; column does not allow nulls. INSERT fails.

que en el caso de Microsoft Access sería:

[Microsoft][ODBC Microsoft Access 97 Driver] The field 'AGENDA.NOMBRE' can't contain a Null value because the Required property for this field is set to True. Enter a value in this field.

En román paladino, el hecho de que la columna NOMBRE esté definida como NOT NULL, hace que no pueda quedarse vacía.

Ahora se verán los pasos que hay que dar para obtener información a partir de una base de datos ya creada. Como se ha dicho anteriormente, se utilizará executeQuery en lugar de execute para obtener resultados. Se sustituyen las líneas que contenían esa sentencia por :

resultado = sentencia.executeQuery( "SELECT * FROM AMIGOS" );
while( resultado.next() ) {
  String nombre = resultado.getString( "NOMBRE" );
  String apellidos = resultado.getString( "APELLIDOS" );
  String cumple = resultado.getString( "CUMPLE" );
  System.out.println( "El aniversario de D. " + nombre + " " 
    + apellidos + ", se celebra el " + cumple );
  }

En este caso, en la primera línea se utiliza executeQuery para obtener el resultado de SELECT * FROM AMIGOS. Mediante resultado.next() la posición se situará en el «siguiente» elemento del resultado, o bien sobre el primero si todavía no se ha utilizado. La función next() devuelve true o false si el elemento existe, de forma que se puede iterar mediante while ( resultado.next() ) para tener acceso a todos los elementos.

A continuación, en las líneas siguientes se utilizan los métodos getXXX() de resultado para tener acceso a las diferentes columnas. El acceso se puede hacer por el nombre de la columna, como en las dos primeras líneas, o bien mediante su ubicación relativa, como en la última línea. Además de getString() están disponibles getBoolean(), getByte(), getDouble(), getFloat(), getInt(), getLong(), getNumeric(), getObject(), getShort(), getDate(), getTime() y getUnicodeStream(), cada uno de los cuales devuelve la columna en el formato correspondiente, si es posible.

Después de haber trabajado con una sentencia o una conexión es recomendable cerrarla mediante sentencia.close() o conexión.close(). De forma predeterminada los drivers JDBC deben hacer un COMMIT de cada sentencia. Este comportamiento se puede modificar mediante el método Connection.setAutoCommit( boolean nuevovalor). En el caso de que se establezca AutoCommit a false, será necesario llamar de forma explícita a Connection.commit() para guardar los cambios realizados o Connection.rollback() para deshacerlos.

Como el lector habrá podido comprobar hasta ahora, no hay nada intrínsecamente difícil en conectar Java con una base de datos remota. Los posibles problemas de conexión que puede haber (selección del driver o fuente de datos adecuada, obtención de acceso, etc.), son problemas que se tendrían de una u otra forma en cualquier lenguaje de programación.

El objeto ResultSet devuelto por el método executeQuery(), permite recorrer las filas obtenidas, no proporciona información referente a la estructura de cada una de ellas; para ello se utiliza ResultSetMetaData, que permite obtener el tipo de cada campo o columna, su nombre, si es del tipo autoincremento, si es sensible a mayúsculas, si se puede escribir en dicha columna, si admite valores nulos, etc.

Para obtener un objeto de tipo ResultSetMetaData basta con llamar al método getMetaData() del objeto ResultSet.

En la lista siguiente aparecen algunos de los métodos más importantes de ResultSetMetaData, que permiten averiguar toda la información necesaria para formatear la información correspondiente a una columna, etc.

getCatalogName()
     Nombre de la columna en el catálogo de la base de datos

getColumnName()
     Nombre de la columna

getColumnLabel()
     Nombre a utilizar a la hora de imprimir el nombre de la columna

getColumnDisplaySize()
     Ancho máximo en caracteres necesario para mostrar el contenido de la columna

getColumnCount()
     Número de columnas en el ResultSet

getTableName()
     Nombre de la tabla a que pertenece la columna

getPrecision()
     Número de dígitos de la columna

getScale()
     Número de decimales para la columna

getColumnType()
     Tipo de la columna (uno de los tipos SQL en java.sql.Types)

getColumnTypeName()
     Nombre del tipo de la columna

isSigned()
     Para números, indica si la columna corresponde a un número con signo

isAutoIncrement()
     Indica si la columna es de tipo autoincremento

isCurrency()
     Indica si la columna contiene un valor monetario

isCaseSensitive()
     Indica si la columna contiene un texto sensible a mayúsculas

isNullable()
     Indica si la columna puede contener un NULL SQL. Puede devolver los valores
     columnNoNulls, columnNullable o columnNullableUnknown, miembros finales
     estáticos de ResultSetMetaData (constantes)

isReadOnly()
     Indica si la columna es de solo lectura

isWritable()
     Indica si la columna puede modificarse, aunque no lo garantiza

isDefinitivelyWritable()
     Indica si es absolutamente seguro que la columna se puede modificar

isSearchable()
     Indica si es posible utilizar la columna para determinar los criterios de búsqueda de un SELECT

getSchemaName()
     Devuelve el texto correspondiente al esquema de la base de datos para esa columna

En general pues, los objetos que se van a poder encontrar en una aplicación que utilice JDBC, serán los que se indican a continuación.

Connection

Representa la conexión con la base de datos. Es el objeto que permite realizar las consultas SQL y obtener los resultados de dichas consultas. Es el objeto base para la creación de los objetos de acceso a la base de datos.

DriverManager

Encargado de mantener los drivers que están disponibles en una aplicación concreta. Es el objeto que mantiene las funciones de administración de las operaciones que se realizan con la base de datos.

Statement

Se utiliza para enviar las sentencias SQL simples, aquellas que no necesitan parámetros, a la base de datos.

PreparedStatement

Tiene una relación de herencia con el objeto Statement, añadiéndole la funcionalidad de poder utilizar parámetros de entrada. Además, tiene la particularidad de que la pregunta ya ha sido compilada antes de ser realizada, por lo que se denomina preparada. La principal ventaja, aparte de la utilización de parámetros, es la rapidez de ejecución de la pregunta.

CallableStatement

Tiene una relación de herencia cn el objeto PreparedStatement. Permite utilizar funciones implementadas directamente sobre el sistema de gestión de la base de datos. Teniendo en cuenta que éste posee información adicional sobre el uso de las estructuras internas, índices, etc.; las funciones se realizarán de forma más eficiente. Este tipo de operaciones es muy utilizada en el caso de ser funciones muy complicadas o bien que vayan a ser ejecutadas varias veces a lo largo del tiempo de vida de la aplicación.

ResultSet

Contiene la tabla resultado de la pregunta SQL que se haya realizado. En párrafos anteriores se han comentado los métodos que proporciona este objeto para recorrer dicha tabla.

Navegador

Home | Anterior | Siguiente | Indice | Correo