Fco. Javier García Castellano


Conexión con Bases de Datos (JDBC)
 


Introducción

JDBC (Java Database Connectivity) es la parte de Java que nos va a permitir conectarnos con bases de datos relacionales utilizando el lenguaje SQL. JDBC permite la integración de llamadas SQL dentro del código de nuestro Servlet Java proporcionando clases que nos permiten interactuar de forma fácil, cómoda y homogénea con bases de datos externas.

En este tutorial seguiremos con el ejemplo de la Facultad de Estudios Avanzados centrándonos en una parte del mismo. Del diagrama Entidad/Relación nos quedaremos con el siguiente trozo para ilustrar el uso de Oracle Developer:

De donde obtenemos las siguientes tablas:

  • Persona: PID, nombre, apellido1, apellido2, direccion, cp, localidad, provincia, telefono, email, lugarNacimiento, fechaNacimiento, NIF, sexo, nacionalidad
  • Alumno: PID, login, password, familiaNumerosa, centroProcedencia, acceso, notaAcceso
  • Profesor: PID, login, password, categoria, fechaAlta, departamento, despacho, telefonoDpcho
  • Matricula: IDMatricula, IDAlumno, tipo, numeroPlazos, fechaExpedicion, curso, PID
  • Pago: IDPago, cantidad, abonado, fechaPago, IDMatricula
  • Asignatura: nombre, creditosTeoricos, creditosPracticos, tipo, cuatrimestre
  • DetalleMatricula: IDMatricula, nombre

Para el diseño físico de la base de datos tenemos tres ficheros:

  • CreaBD.sql: Fichero que contiene el conjunto de sentencias SQL necesario para crear la base de datos.
  • DatosBD.sql: Fichero que contiene el conjunto de sentencias SQL que inicializan la base de datos con unas cuantas tuplas de ejemplo.
  • BorraBD.sql: Fichero que contiene el conjunto de sentencias SQL que borra la base de datos.

1 Conexión con la base de datos con JDBC

Antes de acceder a la base de datos, es necesario conectarse desde nuestro Servlet (Cliente, en este caso) a la base de datos (servidor), esto se hace mediante dos pasos:

  • Leer el driver específico de la base de datos que vamos a utilizar, en nuestro caso, será:
                Class.forName("oracle.jdbc.driver.OracleDriver")
    	
  • Establecer la conexión. Una vez que tenemos el driver cargado en memoria, tendremos que crear una conexión (objeto de la clase Connection), indicando el driver a usar (jdbc:oracle:thin), la máquina(localhost), el puerto(1521), el SID de la base de datos(ProgBD2), el usuario(fjgarcia) y su contraseña(claveSecreta). Por ejemplo:
             Connection conexion = DriverManager.getConnection(
                "jdbc:oracle:thin:@localhost:1521:ProgBD2", "fjgarcia", "claveSecreta");
    
    	

Cuando leemos el driver específico de la base de datos (en nuestro, caso oracle.jdbc.driver.OracleDriver) se puede producir una excepción del tipo (ClassNotFoundException que ocurrirá si no tenemos instalado correctamente el driver JDBC (revisar parte de instalación). Al leer el driver este se quedará en memoria y será el que se utilice internamente por JDBC.

Cuando creamos una conexión (nos devuelve un objeto Connection) se abre una conexión con la base de datos que posteriormente debemos cerrar. Para crear la conexión se utiliza, como hemos visto, el método getConnection de la clase DriverManager, que como vemos tiene tres parámetros, el primero será la cadena de conexión, el segundo el usuario y el tercero la contraseña de dicho usuario. En la cadena de conexión tenemos el driver que se va a utilizar, nosotros siempre usaremos jdbc:oracle:thin la máquina a la que nos vamos a conectar precedida de una arroba (nosotros podremos usar @localhost para nuestro ordenador local o @oracle0.ugr.es para el servidor de la escuela), el puerto a utilizar (tanto local, como remoto usaremos el 1521) y el SID de la base de datos a utilizar (en local será ProgBD2 y en el servidor de la escuela será PRACTBD.

Como hemos mencionado anteriormente para toda conexión que abramos tendremos que cerrarla, eso se hará mediante el método close()de Connection

Veamos un pequeño ejemplo que sólo conecte a la base de datos local para ver si hubiera algún problema y que guardaremos como ConectaBD.java:

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.sql.*;

public class ConectaBD extends HttpServlet {

  public void init(ServletConfig conf)
    throws ServletException {
    super.init(conf);
  }

  public void doGet(HttpServletRequest req, HttpServletResponse res)
    throws ServletException, IOException {

    Connection conexion = null;
    
    res.setContentType("text/html");
    PrintWriter out = res.getWriter();

    try {
	//Leemos el driver de Oracle
	Class.forName("oracle.jdbc.driver.OracleDriver");

	//Nos conectamos a la BD local
	conexion = DriverManager.getConnection (
			"jdbc:oracle:thin:@localhost:1521:PROGBD2",
			"fjgarcia","claveSecreta");

	/*//Nos conectamos a la BD de la ETSII
        String clave="claveSecretaETSII";
        conexion = DriverManager.getConnection (
		       	"jdbc:oracle:thin:@oracle0.ugr.es:1521:PRACTBD",
		  	"fjgarcia",clave);
         */


        //Decimos que nos hemos conectado 
    	out.println("<html>");
    	out.println("<body>");
    	out.println("<h1>¡Hemos conectado!</h1>");
    	out.println("</body>");
	out.println("</html>");

	} 
	catch (ClassNotFoundException e1) {
                //Error si no puedo leer el driver de Oracle 
		out.println("ERROR:No encuentro el driver de la BD: "+
				e1.getMessage());
	}
	catch (SQLException e2) {
                //Error SQL: login/passwd mal
		out.println("ERROR:Fallo en SQL: "+e2.getMessage());
	}
	finally {
                //Finalmente desconecto de la BD
		try {
			if (conexion!=null)
				conexion.close();
		} catch (SQLException e3) {
			out.println("ERROR:Fallo al desconectar de la BD: "+
				e3.getMessage());
		}
	
	}

  }

  public void doPost(HttpServletRequest req, HttpServletResponse res)
    throws ServletException, IOException {
    doGet(req, res);
  }
}

Observa que dentro de las bibliotecas que vamos a usar hemos añadido la línea import java.sql.*; que no sirve para utilizar las clases de JDBC

El resultado si todo ha sido correcto debería ser:


Si no encuentra el driver de Oracle es que no lo ha encontrado en la variable CLASSPATH


Cuando se produce un error SQL (en este ejemplo que no encuentre la BD o que hayamos introducido un usuario/clave incorrectos) se produce una excepción del tipo SQLException. Este tipo de excepción siempre ocurrirá cuando haya un problema fuera de Java y relacionado con la base de datos. Ejemplos típicos son cuando no se puede conectar a la base de datos, hacemos una consulta mal construida, insertamos una tupla violando alguna restricción de integridad, etc. En nuestro ejemplo nos hemos identificado mal el resultado será:


Ten en cuenta que en las aulas de ordenadores de la escuela de informática no hay ninguna base de datos instalada por lo que tendrás que usar, sin otro remedio, el servidor Oracle de la escuela, esto es, oracle0.ugr.es. Por tanto la cadena de conexión, para todos los servlets que crees en la escuela de informática serán del tipo:

   con = DriverManager.getConnection ("jdbc:oracle:thin:@oracle0.ugr.es:1521:PRACTBD", usuario,clave);

EJERCICIO: Hacer un formulario HTML que envíe dos parámetros a un servlet (Usuario/Clave) y éste los utilice para conectarse a la base de datos.

2 Sentencias JDBC

Como con la conexión a la base de datos no nos basta, vamos a ver como se ejecutan sentencias en la misma para consultar datos, actualizar tuplas y añadir/borrar registros.

Una Sentencia JDBC es un objeto Statement (traducido sería Sentencia) de JDBC y se utiliza para mandar sentencias SQL a la Base de Datos y no debería confundirse con una sentencia SQL, es decir, sentencia JDBC (objeto Statement) y sentencia SQL no son lo mismo. Un objeto Statement se asocia con una conexión abierta y no con una sentencia SQL en particular, por tanto, un objeto Statement es una pasarela entre la Base de Datos y nuestro Servlet que nos va a permitir ejecutar una o más sentencias SQL en la Base de Datos.

Para crear un objeto Statement nos hace falta una conexión abierta con la base de datos, por ejemplo:

	//Nos conectamos a la BD local
	conexion = DriverManager.getConnection (
			"jdbc:oracle:thin:@localhost:1521:PROGBD2",
			"fjgarcia","claveSecreta");

	//Creamos una sentencia a partir de la conexión
	Statement sentencia=conexion.createStatement();

Tenemos que tener en cuenta que una sentencia va asociada a una conexión, por lo que si se cierra la conexión se cierra la sentencia. También podemos cerrar las sentencias mediante el método close() de forma análoga como hacíamos con las conexiones.

3 Consultas SQL desde JDBC

Ya sabemos conectarnos y crear sentencias JDBC para poder ejecutar sentencias SQL, veamos como se pueden ejecutar consultas (sentencias SELECT) desde nuestro servlet.

Para poder ejecutar consultas tenemos un método executeQuery (traducido sería ejecutarConsulta) de la clase Statement cuyo parámetro es una cadena con una consulta SQL. Por ejemplo:

	resultados = sentencia.executeQuery("SELECT * FROM Asignatura");

El resultado de la consulta lo almacenará en un objeto de la clase ResultSet (traducido es ConjuntoDeResultados). Por lo tanto, para hacer una consulta nos hará falta crear un objeto Statement que nos permita ejecutar la consulta y objeto ResultSet que nos permita almacenar el resultado de la consulta. Como hemos visto, el objeto Statement nos lo dará el objeto con el cual se ha hecho la conexión. Por ejemplo, si nos fijamos en un trozo del servlet AsignaturasBD.java:

    .....
    Connection conexion = null; //Objeto para la conexión a la BD
    Statement sentencia = null; //Objeto para la ejecutar una sentencia
    ResultSet resultados = null;//Objeto para guardar los resultados

    //Variables conexión
    String cadenaConexion="jdbc:oracle:thin:@oracle0.ugr.es:1521:PRACTBD";
    String cadenaConexion2="jdbc:oracle:thin:@localhost:1521:PROGBD2";
    String usuario="fjgarcia";
    String clave="claveSecreta";

    //La salida será una página HTML
    res.setContentType("text/html");
    PrintWriter out = res.getWriter();

    try {
	//Leemos el driver de Oracle
	Class.forName("oracle.jdbc.driver.OracleDriver");

	//Nos conectamos a la BD
	conexion = DriverManager.getConnection (cadenaConexion,usuario,clave);
	
	//Creamos una sentencia a partir de la conexión
	sentencia=conexion.createStatement();

	//Cogemos todos los datos de la asignaturas
	resultados=sentencia.executeQuery("SELECT * FROM asignatura");
   ....

En el anterior código el resultado de nuestra consulta se debe haber almacenado en el objeto resultados de ResultSet, si hubiera habido algún problema en la ejecución de la consulta nos saltaría una excepción del tipo SQLException.

3.1 Trabajar con los resultados de una consulta

Como hemos visto el método executeQuery de la clase Statement nos devuelve un objeto de la clase ResultSet donde tendremos el conjunto de tuplas resultado de la consulta. La clase ResultSet nos proporciona gran cantidad de métodos para poder movernos entre las tuplas de la consulta y, para cada tupla, entre los distintos atributos que la componen. Por ejemplo, mirando el servlet AsignaturasBD.java:

	....
	//Cogemos todos los datos de la asignaturas
	resultados=sentencia.executeQuery("SELECT * FROM asignatura");

	//Escribimos la cabecera de la página
	out.println("<html>\n <body>\n <h1>Asignaturas</h1>");

	//Mostramos las distintas asignaturas
	while(resultados.next()) { 
	   out.println("<br>Nombre="+resultados.getString("nombre")+
		       " Tipo="+resultados.getString("tipo")+
		       " Cuatrimestre="+resultados.getString("cuatrimestre"));
	}//Fin while

	//Escribimos el final de la página
	out.println("</body>\n </html>");

	....

Si ejecutamos el servlet AsignaturasBD.java el resultado debería ser:


La clase ResultSet tiene un cursor que puede ser usado para movernos entre las distintas filas de los resultados de la consulta y que inicialmente apunta a la primera fila. Podemos usar el método next() (como hemos hecho en el ejemplo anterior) para irnos de una fila a la siguiente, por lo que la llamada de dicho método nos devolverá true si hay una siguiente tupla y false si ya estamos en la última.

Para obtener los distintos atributos de una tupla se utilizarán, normalmente, los métodos getString(atributo) o getObject(atributo), cuyo parámetro es una cadena con el nombre del atributo que queremos recuperar. La diferencia entre ambos métodos es que el primero nos devuelve una cadena (clase String) y el segundo un objeto genérico (clase Object). A partir de la clase Object podemos convertir el atributo al tipo de dato que nos interese aunque también podemos utilizar el método get apropiado. Para más información podemos consultar la Tabla 1:


Tipo de dato SQL Tipo de dato Java devuelto por getObject() Método get apropiado
BIGINT Long long getLong()
BINARY byte[] byte[] getBytes()
BIT Boolean long getLong()
CHAR String String getString()
DATE java.sql.Date java.sql.Date getDate()
DECIMAL java.math.BigDecimal java.math.BigDecimal getBigDecimal
DOUBLE Double double getDouble()
FLOAT Double double getDouble()
INTEGER Integer int getInt()
LONGVARBINARY byte[] InputStream getBinaryStream()
LONGVARCHAR String InputStream getAsciiStream()
InputStream getUnicodeStream()
NUMERIC java.math.BigDecimal java.math.bigDecimal getBigDecimal()
REAL Float float getFloat()
SMALLINT Integer short getShort()
TIME java.sql.Time java.sql.Time getTime()
TIMESTAMP java.sql.Timestamp java.sql.Timestamp getTimestamp()
TINYINT Integer byte getByte()
VARBINARY byte[] byte[] getBytes()
VARCHAR String String getString()
VARCHAR2 String String getString()

TABLA 1:Métodos para extraer los datos de los atributos a partir de un objeto ResultSet

En lugar de utilizar el nombre del atributo, ya que puede que no lo conozcamos, podemos utilizar el número de columna:

	....
	//Mostramos las distintas asignaturas
	while(resultados.next()) { 
	   String nombre = resultados.getString(1);
      	   int creditos = resultados.getInt(2)+resultados.getInt(3);
      	   String cuatrimestre = resultados.getString("cuatrimestre");
      	   String tipo = resultados.getString("tipo");
	  
	   out.println("<br>Nombre="+nombre)+
		       " Créditos totales="+tipo+
		       " Tipo="+tipo+" Cuatrimestre="+cuatrimestre);
	}//Fin while
	....

Hay que tener en cuenta que el objeto ResultSet está ligado al objeto Statement con el que fue creado por lo que si se cierra o se vuelve a usar para otra sentencia SQL el contenido de ResultSet se elimina automáticamente.

También tenemos métodos para saber en que fila nos encontramos como son getRow() (nos da el número de fila), isFirst()(nos pregunta si es la primera), isBeforeFirst()(si estamos justo antes de la primera fila), isLast()(si es la última fila), isAfterLast()(si ya no nos quedan más filas).

Nos puede interesar sabe que datos son nulos pero observa que objetos como getInt() no nos pueden devolver un valor null, para ello la clase ResultSet nos proporciona el método wasNull() para ver si hemos obtenido un valor nulo. Por ejemplo:

	....
	int creditosP = resultados.getInt("CreditosPracticos")	;
	if ( !resultados.wasNull() )
	   out.println(" Créditos Prácticas="+creditosP);
	else
	   out.println(" Sin Prácticas ");
	....

5 Cambiando la Base de Datos con JDBC

Normalmente en la base de datos haremos muchas más operaciones que consultar tablas, pero son operaciones donde no nos esperamos un conjunto de resultados (ResultSet), como son operaciones de inserción, modificación o borrado de tuplas. Para este tipo de sentencias SQL usaremos el método executeUpdate(codigo_sentencia) de la clase Statement. Dicho método nos devolverá el número de tuplas modificadas. Por ejemplo:

	//Creamos una sentencia a partir de la conexión
	sentencia=conexion.createStatement();

	//Insertamos una tupla en la tabla asignaturas
	int n=sentencia.executeUpdate("INSERT INTO Asignatura "+
		     "VALUES('Base de Datos','6,0','4,5','O','1')");

Además de insertar/modificar/borrar tuplas con executeUpdate podemos ejecutar sentencias DDL (data definition language), como crear/borrar/modificar tablas. En sentencias DDL el valor que devuelve el método executeUpdate es siempre cero. Por ejemplo:

	//Creamos una sentencia a partir de la conexión
	sentencia=conexion.createStatement();

	//Creamos una tabla Insertamos una tupla en la tabla asignaturas
	sentencia.executeUpdate(
		"CREATE TABLE Asignatura ("+
		   "nombre VARCHAR(50) PRIMARY KEY,"+ 
		   "creditosTeoricos NUMBER(2,1) NOT NULL,"+ 
		   "creditosPracticos NUMBER(2,1) NOT NULL,"+ 
		   "tipo CHAR(1) NOT NULL CHECK (tipo IN ('O','T','P','L')),"+  
		   "cuatrimestre NUMBER(1)"+ 
		");");
	//Hacemos un commit
	conexion.commit();

Además de executeQuery y executeUpdate, tenemos el método genérico execute(cadena_sentencia) que nos devuelve true si tiene algún ResultSet para devolvernos y false en caso de que no devuelva un ResultSet. Es decir, devuelve verdadero si es una sentencia de consulta y falso si es otro tipo de sentencia. Si no es una sentencia de consulta podemos acceder al número de tuplas que se han modificado con la sentencia usando el método getUpdateCount(), en el otro caso, se ha ejecutado una consulta, por lo que para poder acceder al ResultSet que devuelve la sentencia ejecutada podemos usar el método getResultSet().

Ten en cuenta que cada vez que modifiquemos la base de datos tendremos que hacer un commit para que los cambios e hagan permanentes, para ello usaremos el método commit() de la conexión (clase Connection).