Primeros Pasos Con JDBC

74
Primeros pasos con JDBC Vamos a continuar con nuestro ejemplo sobre base de datos MySQL, aunque casi todo lo que se dice aquí sirve para otros fabricantes. Introducción a JDBC La idea que está detrás de JDBC (Java Data Base Connectivity) es dar a los programadores un API (Application Programming Interface) que les permita codificar de manera independiente al fabricante del gestor de base de datos. JDBC tiene dos capas: por una parte el API JDBC y por otra el controlador del fabricante, este último recibe las peticiones de JDBC y las traduce en servicios internos del gestor de bases de datos: 1. El API JDBC que es el conjunto de clases que utiliza el programador. Dentro de este API tenemos el administrador de controladores (Driver Manager) que recibe las peticiones del programa Java y comunica con el controlador seleccionado, de manera transparente para el programador. 2. El controlador recibe las ordenes y las traduce a ordenes directas al gestor de base de datos. Hay productos (escasísimos) para los que no hay controlador JDBC, pero si un controlador ODBC. En estos casos se utiliza entre el administrador y el controlador ODBC un puente (denominado normalmente JDBC-ODBC Bridge). Tipos de controladores Cuando accedas a la literatura especializada veras que se usa indistintamente la expresión "tipo" o "nivel" (level). Un controlador de nivel 3 es lo mismo que decir un controlador de tipo 3: 1. Controlador que traduce JDBC a ODBC. SUN incluye uno en su JDK. Exige la instalación y configuración de ODBC en la computadora cliente. 2. Controlador escrito parcialmente en Java y en código nativo. Se instala en la computadora cliente software Java y código binario. 3. Controlador de Java puro que utiliza un protocolo de red (http, por ejemplo) para comunicarse con un servidor de base de datos (por ello se denominan controladores de protocolo no nativo). Este servidor traduce al lenguaje específico del producto. No exige instalación en cliente.

Transcript of Primeros Pasos Con JDBC

Page 1: Primeros Pasos Con JDBC

Primeros pasos con JDBC

Vamos a continuar con nuestro ejemplo sobre base de datos MySQL, aunque casi todo lo que se dice aquí sirve para otros fabricantes.

Introducción a JDBC

La idea que está detrás de JDBC (Java Data Base Connectivity) es dar a los programadores un API (Application Programming Interface) que les permita codificar de manera independiente al fabricante del gestor de base de datos. JDBC tiene dos capas: por una parte el API JDBC y por otra el controlador del fabricante, este último recibe las peticiones de JDBC y las traduce en servicios internos del gestor de bases de datos:

1. El API JDBC que es el conjunto de clases que utiliza el programador. Dentro de este API tenemos el administrador de controladores (Driver Manager) que recibe las peticiones del programa Java y comunica con el controlador seleccionado, de manera transparente para el programador.

2. El controlador recibe las ordenes y las traduce a ordenes directas al gestor de base de datos.

Hay productos (escasísimos) para los que no hay controlador JDBC, pero si un controlador ODBC. En estos casos se utiliza entre el administrador y el controlador ODBC un puente (denominado normalmente JDBC-ODBC Bridge).

Tipos de controladores

Cuando accedas a la literatura especializada veras que se usa indistintamente la expresión "tipo" o "nivel" (level). Un controlador de nivel 3 es lo mismo que decir un controlador de tipo 3:

1. Controlador que traduce JDBC a ODBC. SUN incluye uno en su JDK. Exige la instalación y configuración de ODBC en la computadora cliente.

2. Controlador escrito parcialmente en Java y en código nativo. Se instala en la computadora cliente software Java y código binario.

3. Controlador de Java puro que utiliza un protocolo de red (http, por ejemplo) para comunicarse con un servidor de base de datos (por ello se denominan controladores de protocolo no nativo). Este servidor traduce al lenguaje específico del producto. No exige instalación en cliente.

4. Controlador de Java puro con protocolo nativo. El controlador se comunica con un servidor de base de datos por medio de un protocolo específico a su marca. No exige instalación en cliente.

Los niveles 1 y 2 se usan normalmente cuando no queda otro remedio, porque el único sistema de acceso final al gestor de bases de datos es ODBC, pero exigen instalación de software en el puesto cliente. En la mayoría de los casos la opción más adecuada será el nivel 4.

Dentro del paquete java.sql tenemos la clase Driver, que es el controlador específico a la base de datos. Por cierto, en la actualidad ya existen auténticas bases de datos Java, como Cloudscape de IBM. El que la base de datos este implementada en Java nos permite escribir en este lenguaje los procedimientos almacenados, además de evolucionar de un modelo relacional a un modelo orientado a objetos.

Page 2: Primeros Pasos Con JDBC

Instalación de MySQL Connector/J

MySQL Connector/J es una implementación de JDBC 3 para el servidor de bases de datos MySQL. No soporta JDK 1.1 ni 1.0. Para compilar requiere JDK 1.4, aunque puede ejecutarse sobre 1.2 y 1.3. Se puede descargar en la página de descargas de MySQL. La documentación puede verla en la página de documentación de MySQL.

Dentro del software descargado se encuentra el archivo jar: mysql-connector-java-[versión]-bin.jar, que contiene todas las clases necesarias. Lo primero es instalar el archivo jar en la variable CLASSPATH. Se puede hacer desde la línea de ordenes:

C:\> set CLASSPATH=\path\to\mysql-connector-java-[version]-bin.jar;%CLASSPATH%

O bien instalarlo en el sistema Windows: Inicio-Panel de control-Sistema-Opciones Avanzadas-Variables de entorno:

Si no quiere modificar la variable CLASSPATH tendrá que copiar el archivo jar en el directorio $JAVA_HOME/jre/lib/ext.

El nombre de la clase que implementa el controlador de MySQL es "com.mysql.jdbc.Driver" y se encuentra dentro del archivo jar.

Volver al índice

Conexión a la base de datos

Introducción

El siguiente objetivo es conectarse a la base de datos, para lo cual básicamente hay que realizar dos pasos:

1. Registrar (cargar en memoria) el controlador. Esto se puede hacer de dos formas:

o De forma dinámica, por medio del método Class.forName( String de driver ). Esta es la forma más usual y la que usaremos en la mayor parte de los ejemplos.

Ejemplos de carga dinámica:

En MySQL local: Class.forName("com.mysql.jdbc.Driver"); En Oracle: Class.forName("oracle.jdbc.Driver.OracleDriver");

Page 3: Primeros Pasos Con JDBC

o De manera estática, usando System.setProperties("jdbc.drivers", String de driver).

2. Establecer la conexión por medio del método DriverManager.getConnection( String de la URL de base de datos, String de login, String de password ). Este método es static, por ello no necesitaremos instanciar un objeto de tipo DriverManager para hacer la llamada.

La conexión usa una sintaxis semejante a la de una URL. La sintaxis de la URL (los corchetes indican elementos opcionales) es la siguiente:

jdbc:Controlador://[host][,failoverhost...][:port]/[database][?propertyName1][=propertyValue1][&propertyName2][=propertyValue2]...

Notas a la sintaxis de la URL:

o Controlador: controlador de base de datos, en nuestro ejemplo es mysql. o Host: servidor de base de datos (por defecto es localhost). o Failoverhost: servidor de respaldo, en caso de que falle el primero o Port: si no se indica puerto, se toma el valor por defecto. En el caso de MySQL

es 3306. o Database:nombre de la base de datos. Si no se indica base de datos, asume la

base de datos actual. Por medio del método setCatalog de la clase Connection se puede determinar la base de datos actual.

o Una curiosidad para los que vayan a aprender a crear servlets: los símbolos '?', '=' y '&' tienen la misma función que en las llamadas de tipo GET a los servlets, respectivamente: la interrogación señala que a continuación tenemos la primera propiedad (propertyName1), el 'igual' separa el nombre de la propiedad de su valor (propertyValue) y, por último, el '&' antecede a las siguientes propiedades.

Ejemplos de conexión:

o En MySQL local: Connection con = DriverManager.getConnection( "jdbc:mysql://localhost/prueba", "log", "pwd" );

o En Oracle: Connection con = DriverManager.getConnection( "jdbc:oracle:thin:servidor:1521:prueba", "log", "pwd" );

Un sencillo ejemplo con registro dinámico

A continuación el primer ejemplo (es una sencilla aplicación en modo texto) con registro dinámico del controlador (driver) y la conexión mediante DriverManager.getConnection, en la llamada a este método el segundo y tercer argumento son el login y el password. Contiene comentarios detallados:

package jdbc01;

import java.sql.DriverManager;import java.sql.Connection;import java.lang.ClassNotFoundException;

Page 4: Primeros Pasos Con JDBC

import java.sql.SQLException;

public class jdbc01_conexion2 {public static void main(String[] args) {

try {

/**** Cargamos el driver ****/Class.forName("com.mysql.jdbc.Driver");

/**** Realizamos la conexión ****/Connection con =

DriverManager.getConnection( "jdbc:mysql://localhost/prueba", "root", "palabra" );

/**** Ok: avisamos ****/System.out.println( "Si he llegado hasta

aquí es que se ha producido la conexión");System.out.println( "Si no se hubiera

producido, se habría disparado SQLException");

/**** Una buena costumbre: cerramos la conexión ****/

con.close();}/**** Excepción que se dispara si falla la

carga del driver ****/catch( ClassNotFoundException e )

{ e.printStackTrace(); }

/**** Excepción que se dispara si falla la conexión *****/

catch ( SQLException e) { e.printStackTrace(); }

}}

Aunque el código del ejemplo anterior está documentado, vamos a repasar algunos conceptos:

Class.forName: si esta llamada falla (normalmente porque no se encuentra el archivo del controlador), entonces se dispara una excepción ClassNotFoundException.

De manera semejante, si el intento de conexión mediante DriverManager.getConnection ha fallado se dispara la excepción SQLException. La gestión de esta excepción verás que no sólo es necesaria en la conexión a la base de datos, sino en otras circunstancias: definición de datos, manipulación de datos, consulta, etc.

Un aspecto que ya hemos comentado: puesto que getConnection() es static no resulta necesario instanciar la clase DriverManager.

Por último, no olvidar la higiene: cerramos la conexión. Ahora puede perecer algo paranoico (no lo es). Pero además, a medida que las aplicaciones sean más complejas la disciplina de cierre de conexiones es más necesaria.

El resultado final es el esperado, aparece por pantalla:

Si he llegado hasta aquí es que se ha producido la conexiónSi no se hubiera producido, se habría disparado SQLException

Un ejemplo con registro estático

En el segundo ejemplo podemos observar:

Page 5: Primeros Pasos Con JDBC

Los datos de registro del controlador (driver) y de la conexión (base de datos, usuario y password) se encuentran en el archivo "database.properties", cuyo contenido es:

jdbc.drivers=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost/prueba jdbc.username=root jdbc.password=palabra

Para cargar en memoria esta información el proceso es muy simple, tanto que se puede hacer todo en tres líneas:

1. Instanciamos la clase Properties: Properties props = new Properties(); 2. Creamos un flujo de entrada para el archivo: FileInputStream in = new

FileInputStream("database.properties"); 3. Cargamos el contenido en props: props.load(in);

Obtener una propiedad en concreto del objeto props es muy simple, por ejemplo: String url = props.getProperty("jdbc.url");

El registro se realiza de manera estática, por medio de System.setProperties("jdbc.drivers", String de driver).

La conexión se realiza, al igual que en el ejemplo anterior, por medio de DriverManager.getConnection(...).

import java.sql.DriverManager;import java.sql.Connection;import java.sql.SQLException;import java.io.FileInputStream;import java.io.IOException;import java.util.Properties;

public class jdbc01_conexion3 {public static void main(String[] args) {

try { /*** Abrimos flujo de entrada y cargamos

el contenido en props ****/Properties props = new Properties();FileInputStream in = new

FileInputStream("database.properties");props.load(in); // Cargamos el contenido

del flujo en propsin.close();

/**** Obtenemos las propiedades del objeto props ***/

/*** Para registrar de manera estática (setProperty) el driver ***/

String drivers = props.getProperty("jdbc.drivers");

if (drivers != null)

System.setProperty("jdbc.drivers", drivers); // Cargamos driver

/*** Para la conexión ***/

Page 6: Primeros Pasos Con JDBC

String url = props.getProperty("jdbc.url");

String username = props.getProperty("jdbc.username");

String password = props.getProperty("jdbc.password");

/*** Conexión ****/Connection con =

DriverManager.getConnection( url, username, password );}catch (SQLException e)

{ e.printStackTrace(); }catch (SecurityException e)

{ e.printStackTrace(); }catch (IOException e) { e.printStackTrace(); }

}}

Archivo log

En el siguiente ejemplo utilizamos un registro dinámico y además utilizamos un archivo .log para ver los eventos que suceden en la conexión JDBC, para lo cual utilizamos la función setLogWriter():

import java.sql.DriverManager;import java.sql.Connection;import java.lang.ClassNotFoundException;import java.sql.SQLException;import java.io.FileOutputStream;import java.io.File;import java.io.IOException;import java.io.PrintWriter;

public class jdbc01_conexion4 {public static void main(String[] args) {

try {Class.forName("com.mysql.jdbc.Driver");activar_log( "ejemplo.log" );Connection con =

DriverManager.getConnection( "jdbc:mysql://localhost/prueba", "root", "palabra" );

}catch( ClassNotFoundException e )

{ e.printStackTrace(); }catch (SQLException e)

{ e.printStackTrace(); }}

public static void activar_log( String archivo ) {try {

FileOutputStream fs = new FileOutputStream(new File(archivo));

PrintWriter p = new PrintWriter(fs, true);

DriverManager.setLogWriter(p);}catch (IOException e) { e.printStackTrace(); }

}

Page 7: Primeros Pasos Con JDBC

}

En el archivo log aparece la carga de controlador y la conexión:

DriverManager.getConnection("jdbc:mysql://localhost/prueba")trying

driver[className=com.mysql.jdbc.Driver,com.mysql.jdbc.Driver@5224ee]getConnection returning driver[className=com.mysql.jdbc.Driver,com.mysql.jdbc.Driver@5224ee]

Si tienes curiosidad, puedes provocar un error de conexión y verás que aparece descrito en el archivo log.

Volver al índice

Servlet que prueba la conexión a una base de datos

(Febrero de 2005)

Introducción

Vamos a realizar un sencillo servlet que realiza una conexión a base de datos. Si la conexión ha tenido éxito, la cierra y muestra el resultado en la página. Damos por supuesto que el lector está informado de los fundamentos de JDBC. Primero trataremos el formulario y después comentaremos el código fuente del servlet.

El formulario que invoca al servlet es el siguiente:

Base:     

Usuario:  

Password:

Si ves el código fuente, hay varios aspectos a tener en cuenta en el formulario:

El driver y el host estan definidos en un archivo .properties. El servlet se apoya en la la clase Propiedades para leer las propiedades del archivo.

El método de llamada es POST y recibe tres tarámetros:o base: es el nombre de la base de datos. Tipo: text.o login: es el login del usuario. Tipo: text.o password: es el password del usuario. Tipo: PASSWORD

Manejo de un archivo .properties

proactiv_prueba

proactiv_alumnos

*******

Entrar

Page 8: Primeros Pasos Con JDBC

En el código del servlet puede observar los atributos de la clase:

public class Conexion extends HttpServlet { private Connection con = null; // Conexion private boolean driver = false; // true si se ha cargado driver en init() private Propiedades acceso; // Clase para saber driver, host, etc.

....

El primer atributo es una referencia a la conexión. El segundo es una bandera (flag) que nos sirve para indicar que se ha producido un error al cargar el driver en init(). El tercero es una referencia a un objeto de la clase Propiedades. Veremos lo que hace esta clase. Pero antes veremos lo que hace el método init() de nuestro servlet:

public void init(ServletConfig config) throws ServletException { super.init(config); try {

ServletContext sc = config.getServletContext(); // Obtengo contexto del servlet

//// El path donde está el archivo de propiedades es 'contexto/propiedades/paquete/'

//// sc.RealPath("/") me da el path del contexto de aplicación acceso = new Propiedades( sc.getRealPath("/")+"propiedades/" +

getClass().getPackage().getName()+"/");

//// Si no hay problema con el archivo de propiedades, cargo el driver

if ( acceso.mensajeError == null ) { Class.forName( acceso.getDriver() ); driver = true; }

} catch (ClassNotFoundException e) {

driver = false; } }

El método empieza obteniendo un contexto del servlet, que llamamos sc. La utilidad de este contexto es lograr el path del contexto de aplicación por medio de sc.getRealPath("/"). Este directorio es la ruta raiz que nos da el servidor de aplicaciones. Recuerde que en la versión de Tomcat 4.X, si trabaja con el contexto raiz, este path será CATALINA_HOME/webapps/ROOT.

El constructor de Propiedades nos pide el path del archivo parametros.properties que contiene en la forma de pares clave-valor el driver y host al que conectaremos:

basedatos.driver=com.mysql.jdbc.Driverbasedatos.host=jdbc:mysql://localhost:3306/

Es evidente que es una conexión a un servidor local, si utilizásemos un servidor remoto:

basedatos.driver=org.gjt.mm.mysql.Driverbasedatos.host=jdbc:mysql://proactiva-calidad.com:3306/

Page 9: Primeros Pasos Con JDBC

Este archivo se encuentra en la ruta PATH_CONTEXTO/propiedades/NOMBRE_PAQUETE. Nuestro paquete se llama docen_servlet01 y lo obtenemos por medio de getClass().getPackage().getName(). Por tanto (si usamos el contexto raíz) el path completo es: CATALINA_HOME/webapps/ROOT/propiedades/docen_servlet01, que se obtiene mediante:

sc.getRealPath("/")+"propiedades/" + getClass().getPackage().getName()+"/"

Esta es la cadena que pasamos al constructor de la clase Propiedades.

La clase Propiedades es muy sencilla:

/**************************************************************************** * Clase que lee propiedades de un archivo .properties * El constructor carga las propiedades. Si hay error, queda almacenado en mensajeError; * si no lo hubiere, mensajeError permanece siendo null. ****************************************************************************/public class Propiedades {

private String ficheroParametros = "parametros.properties"; private Properties prop = new Properties(); public String mensajeError;

/*************************************************************************** * Constructor que carga en atributo 'prop' el archivo de propiedades ***************************************************************************/ public Propiedades( String pathContexto ) { try {

URL url = new URL( "file:" + pathContexto + ficheroParametros ); // Abro URL

prop.load( url.openStream() ); // Cargo propiedades desde InputStream de URL } catch (MalformedURLException e) {

mensajeError = new String("Mensaje de error: " + e.toString() ); } catch (IOException e) {

mensajeError = new String("Mensaje de error: " + e.toString() ); } }

/*************************************************************************** * Método que recupera el valor de una clave del atributo 'prop'.

Page 10: Primeros Pasos Con JDBC

* Si no la encuentra, devuelve el parámetro 'defecto' ***************************************************************************/ public String getParametro(String clave, String defecto) { String retorno = defecto; try {

retorno = prop.getProperty(clave, defecto); } catch (Exception e) {

retorno = defecto; } finally {

return retorno; } } /**************************************************************************** * Sobrecargado. Si no encuentra clave, devuelve "" ****************************************************************************/ public String getParametro(String clave) { return getParametro(clave, ""); }

public String getDriver() { return getParametro( "basedatos.driver" ); } public String getHost() { return getParametro( "basedatos.host" ); }}

Puede observarse que el constructor consigue una URL al archivo por medio de:

URL url = new URL( "file:" + pathContexto + ficheroParametros ); // Abro URL

¿Por qué se añade el protocolo "file:"? Sencillo, nuestro archivo .properties se encuentra en el propio contexto de aplicación del servlet y, por tanto, no necesita de usar el protocolo http. Las propiedades se cargan en el atributo prop por medio de una llamada a load(), donde el argumento es el InputStream de la URL.

Una vez que se han cargado las propiedades del archivo, la clase puede devolver una propiedad mediante la llamada a:

retorno = prop.getProperty(clave, defecto);

getProperty() nos devuelve el valor de la clave correspondiente, si no la encontrase devuelve el segundo argumento (defecto). Por ejemplo, en nuestro caso:

retorno = prop.getProperty("basedatos.driver", defecto);

Almacenamos en retorno el texto "com.mysql.jdbc.Driver".

Page 11: Primeros Pasos Con JDBC

Volvamos a init()

Con lo que sabemos estamos en condiciones de entender lo que hace el método init(). Una vez que he creado el objeto acceso, de la clase Propiedades, podemos llamar a acceso.getDriver() para obtener el driver señalado en el archivo .properties.

//// Si no hay problema con el archivo de propiedades, cargo el driver

if ( acceso.mensajeError == null ) { Class.forName( acceso.getDriver() ); driver = true; }

Es importante recordar que init() sólo se ejecuta en la primera invocación al servlet. Por tanto cualquier modificación al archivo .properties surtirá efecto una vez que recarguemos el servlet (el manager de Tomcat viene con una opción de 'reload').

doPost

En doPost() respondemos a cada invocación al servlet desde el formulario HTML. La tarea más importante que se realiza aquí es la apertura de una conexión a la base de datos. ¿Dónde poner la conexión a la base de datos? ¿En init() o en doXX()? Dicho de otra forma, ¿nos conectamos en init() o cada vez que se hace una solicitud (un "request" o invocavión al servlet)? Lo normal es que queramos que cada usuario abra una conexión a la base de datos, por tanto intentaremos la conexión en respuesta a una solicitud get o post. Si pusieramos la conexión en el init(), dicha conexión sólo se realizaría la primera vez que se invocase el servlet.

En doPost() realizamos las siguientes tareas:

1. Empezamos con lo habitual, definiendo el tipo de salida: 2.3. response.setContentType("text/html; charset=iso-8859-1");

// Definir tipo de salida4. PrintWriter out = response.getWriter();

// Obtener flujo salida

5. A continuación se imprime el inicio de página: 6.7. try {8. //// Imprimir inicio página9. out.println("");10. ....

11. Comprobamos que el driver se ha cargado correctamente, avisando al usuario. Si la carga no fue correcta terminamos la página (escribimos etiquetas de fin) y salimos (return) del método:

12.13. if ( !driver ) {14. out.println("&ltbr&gtNo se ha cargado el driver");15. terminarPagina( out );16. return;17. }

Page 12: Primeros Pasos Con JDBC

18. Realizamos la conexión a la base de datos por medio del mensaje DriverManager.getConnection() que lanza una excepción del tipo SQLException si hay un error. Aquí es interesante observar que los argumentos de este mensaje se obtienen de los parámetros del formulario HTML, por medio de request.getParameter("nombre_del_parámetro"); salvo el host, que se obtiene del objeto de la clase Propiedades: acceso.getHost(). La llamada completa es:

19.20. con = DriverManager.getConnection( acceso.getHost()

+21.

request.getParameter("base"),22.

request.getParameter("login"),23.

request.getParameter("password"));

24. Cerramos la conexión y terminamos la página.. El método cerrarConexion() cierra la conexión (si es que no está ya cerrada) y devuelve true en caso de que el cierre haya sido normal o false en caso de que haya habido errores. Acabamos poniendo el fin de la página (etiquetas de cierre de página). Recordar que la sentencia finally {} se ejecuta en cualquier caso (haya habido o no excepciones).

25.26. finally {27. if ( cerrarConexion() )28. out.println( "&ltP&gtCerrada

conexión.</P>");29. else30. out.println( "&ltP&gtError en cierre de

conexión.</P>");31. terminarPagina( out );32. }

destroy() y cierre de conexión

En respuesta a la destrucción del servlet se llama a cerrarConexión(). El código es el siguiente:

public void destroy() { cerrarConexion(); }

boolean cerrarConexion() { try { if ( con != null ) { if (!con.isClosed()) con.close(); } return true; } catch (SQLException e) { return false; } }

cerrarConexión() hace lo siguiente:

Page 13: Primeros Pasos Con JDBC

Si la conexión está cerrada no hace nada. Si la conexión está abierta, la cierra. Si al intentar cerrarla se ha producido un error,

entonces se dispara una excepción SQLException.

Volver al índice

Servlets

Ramiro Lago (Octubre 2005)

En este capítulo nos centramos en el mundo de los servlets, de imprescindible conocimiento para quien quiera entender y desarrollar aplicaciones empresariales. Para entender el papel de los servlets dentro de la arquitectura J2EE puede consultar el capítulo dedicado a dicha arquitectura.

1. Introducción a los servlets (*). Realizaremos el clásico "Hola mundo". Se aborda también el problema de despliegue de servlets y la instalación de contextos de aplicación con Tomcat.

2. Métodos y clases fundamentales (*). Usaremos los métodos init(), doPost(), doGet() y destroy(). Dentro de init(), entre otras cosas, se obtienen parámetros de inicio introducidos en web.xml. Dentro de doGet() se obtienen las propiedades de System.

3. Uso de HttpServletRequest (*). Se puede ver la cantidad de información que podemos obtener a partir de HttpServletRequest, la clase que encapsula datos de la petición.

4. Servlet que se conecta a una base de datos (utiliza JDBC) (*). Además presenta un ejemplo del manejo de la clase Properties.

5. destroy(): cómo terminar de manera elegante (y segura)

6. Manejo de información de la sesión (*) El primer servlet crea un atributo de sesión, mediante setAttibute(), el segundo lo obtiene mediante getAttribute().

Page 14: Primeros Pasos Con JDBC

7. Uso de sendRedirect() (*). Un servlet no tiene necesariamente que generar una salida en HTML, en muchas ocasiones realizan una acción e invocan (mediante sendRedirect) a un segundo servlet. Además se muestra el uso de códigos de error en la respuesta.

8. RequestDispatcher (*).

9. Manejo de cookies (*).

10. Filtros . El filtro se interpone en la petición HTTP y puede validarlas.

11. Servlets que utilizan JDBC y atributos de sesión (*). Un ejemplo de anidamiento de servlets con HTML. Un servlet genera un formulario en HTML (lista de clientes), este formulario invoca a un segundo servlet (ventas del cliente seleccionado) que da la página de respuesta final. El acceso a la base de datos lo realizan clases DAO. Además se maneja la sesión. Hay un ejemplo de uso de DecimalFormat.

12. Servlet para la carga (upload) de archivos . .

13. Por medio de JNDI un servlet puede obtener información de web.xml (*), concretamente las propiedades de entorno (enviroment) definidas por el administrador. Es una forma más sencilla de obtener propiedades que el manejo de archivos .properties.

14. Por medio de JNDI un servlet puede acceder a un pool de conexiones , para lo cual debemos usar el interfaz DataSource.

Enlace a un tutorial de SUN sobre Servlets.

* Capítulos que contienen servlets de ejemplo.

Volver al índice

Consultas a la base de datos

Después de haber estudiado las formas de conexión a la base de datos vamos a aprender a realizar consultas a las tablas. Este capítulo da por supuesto que el lector ya tiene nociones

Page 15: Primeros Pasos Con JDBC

básicas de modelo relacional de datos y de SQL. Pero no nos preocupemos, la mecánica para poder realizar consultas es bastante sencilla.

La estructura de la base de datos

Lo primero es conocer la estructura de la base de datos 'prueba':

La primera tabla se llama cliente, representa la información esencial de cada cliente, en donde el campo 'código' es la clave primaria. Ha sido creada con la siguiente sentencia SQL:

CREATE TABLE `cliente` ( `codigo` char(10) NOT NULL default '', `nombre` char(20) default '', `ape1` char(20) default '', `ape2` char(20) default '', `edad` int(11) default '0', PRIMARY KEY (`codigo`) ) TYPE=InnoDB;

La segunda tabla representa las ventas o facturación de cada cliente. La relación entre la entidad 'cliente' y la entidad 'venta' es del tipo 1:N, es decir, a un cliente se le puede asignar N ventas, pero una venta se refiere sólo a un cliente. Por tanto, el campo 'código' es una clave externa sobre la clave primaria 'cliente.codigo'. La tabla ha sido creada por medio de:

CREATE TABLE `venta` ( `codigo` char(10) NOT NULL default '', `precio` float default '0', `coste` float default '0', INDEX(`codigo`), FOREIGN KEY (`codigo`) REFERENCES `cliente` (`codigo`) ON

DELETE CASCADE ON UPDATE CASCADE ) TYPE=InnoDB;

Con 'ON DELETE CASCADE' le ordenamos al gestor de base de datos que si se borra un cliente, entonces se borrarán los registros de ventas de ese cliente. Con ON UPDATE CASCADE indicamos que un cambio en el código del cliente ('cliente.codigo') implica reflejar el cambio en 'venta.codigo' con la finalidad de mantener la integridad referencial.

Nota sobre MySQL: para manejar claves externas (foreign key) en MySQL por debajo de versiones 5.1 es necesario que las tablas sean del tipo InnoDB. A partir de la versión 5.1 esta característica es soportada por todos los tipos de tablas.

Los datos que tenemos en las tablas reflejan que hay tres clientes y hemos realizado cinco ventas con ellos:

Definir y ejecutar sentencias SQL

Definir y ejecutar sentencias SQL conlleva un proceso:

Page 16: Primeros Pasos Con JDBC

1. Lo primero es escribir una sentencia, almacenándola en un String. En nuestro siguiente ejemplo es un sencillo listado de clientes, ordenado por el nombre:

2.3. String orden_SQL = "SELECT cliente.codigo, cliente.nombre,

cliente.edad FROM cliente ORDER BY cliente.nombre";4. En segundo lugar, debemos crear un objeto de la clase Statement, por medio de una

llamada a createStatement() de la clase Connection (recuérdese que el objeto de la clase Connection lo obtuvimos al conectarnos a la base de datos):

5.6. Statement sentencia = con.createStatement();7. A continuación se llama al método executeQuery( String ) de la clase Statement. Si

nuestra sentencia implicase una modificación de datos (INSERT, UPDATE o DELETE) el método sería executeUpdate( String ):

8.9. ResultSet rs = sentencia.executeQuery( orden_SQL );

Respecto a executeQuery( String ) conviene resaltar que nos devuelve un objeto de la clase ResultSet, es decir, un objeto que "encapsula" los datos resultantes de la consulta. Nos permite manejar fila por fila el resultado de la consulta.

10. Hay que definir un bucle para obtener el resultado (fila a fila) de la consulta. Para ello utilizamos la función ResulSet.next(). Ojo: el cursor del conjunto de resultados se situa en la posición anterior a la primera fila, por tanto habrá que llamar a esta función para obtener la primera fila:

11.12. while ( rs.next() ) {13. String res = rs.getString( "codigo" ) + ", " +

rs.getString( "nombre" ) + ", " + rs.getInt( "edad" );14. System.out.println( res );15. }

Dentro del bucle obtenemos los resultados de cada columna, por medio de funciones getXXX( String ) de la clase ResulSet. El parámetro String representa el nombre de la columna. En función del tipo de dato habrá que escoger el nombre de la función: getInt() para enteros, getString para String, etc. En nuestro ejemplo, los datos obtenidos se muestran por pantalla usando println().

16. Es necesario cerrar el objeto de la clase Statement: sentencia.close(). El cierre del objeto Statement genera automáticamente el cierre del objeto ResultSet. El objeto ResultSet también se cierra automáticamente cuando se va a producir su recolección como basura (garbage collection).

17. No hay que olvidar el sempiterno manejo de escepciones: 18.19. catch (SQLException e) { e.printStackTrace(); }

20. Nos aseguramos del cierre de la conexión: con.close(). En nuestro caso, hemos tomado precauciones. Puede ocurrir que la conexión haya sido correcta pero se produce un error en una sentencia. Ante este hecho, hay que asegurarse que se produce el cierre de la conexión. Para ello situamos en finally una llamada a nuestro método cerrar_conexion():

21.22. public static void cerrar_conexion( Connection con ) {23. try {24. if ( con != null )25. if ( !con.isClosed() ) // Si no está

cerrada, la cierro

Page 17: Primeros Pasos Con JDBC

26. con.close();27. }28. catch (SQLException e) { e.printStackTrace(); }29. }

Para nuestro ejemplo hemos creado una clase que llamamos 'consulta', cuyo constructor recibe la conexión con la base de datos. La clase 'consulta' está especializada en la definición y ejecución de las consultas SQL. El código completo del ejemplo lo puedes ver más abajo. En la función ver_cliente() se implementa la sencilla consulta SELECT que acabamos de ver:

package jdbc01;import java.sql.DriverManager;import java.sql.Connection;import java.sql.Statement;import java.sql.ResultSet;import java.sql.SQLException;import java.lang.ClassNotFoundException;import java.io.FileOutputStream;import java.io.File;import java.io.IOException;import java.io.PrintWriter;/************************************************************************ Ejemplo de manejo de sentencias SELECT*************************************************************************/public class jdbc01_conexion4 { public static void main(String[] args) {

Connection con = null;try { /*** Registro de driver ****/ Class.forName("com.mysql.jdbc.Driver");

/*** Crear conexión con base de datos ***/ con = DriverManager.getConnection(

"jdbc:mysql://localhost/prueba", "root", "palabra" );

/*** Con el objeto c ejecutamos sentencias SQL y visualizamos resultados ***/

consulta c = new consulta( con ); System.out.println( "Listado de todos los clientes:"); c.ver_cliente(); System.out.println( "Listado de los clientes entre 21 y 30

años:"); c.ver_cliente( 21, 30); System.out.println( "Listado de los clientes entre 21 y 30

años con sus ventas correspondientes:"); c.ver_cliente_venta( 21, 30); System.out.println( "Listado de ingresos, costes y

beneficio de clientes:"); c.ver_beneficio_cliente();}/*** Capturo excepciones ***/catch( ClassNotFoundException e ) { e.printStackTrace(); }catch (SQLException e) { e.printStackTrace(); }

/*** Haya excepción o no, tengo que cerrar la conexión ***/finally { cerrar_conexion( con );

Page 18: Primeros Pasos Con JDBC

} }

/******************************************************************** Me aseguro de que se cierra la conexión ********************************************************************/ public static void cerrar_conexion( Connection con ) {

try { if ( con != null )

if ( !con.isClosed() ) // Si no está cerrada, la cierro

con.close();}catch (SQLException e) { e.printStackTrace(); }

}}

/********************************************************* Clase en la que definimos y ejecutamos las sentencias SQL**********************************************************/class consulta { Connection con; consulta( Connection con ) {

this.con = con; }

/******************************************************************* Un sencillo SELECT de clientes, ordenados por nombre *******************************************************************/ void ver_cliente() {

try { /***** Definir sentencia y ejecutarla ********/ String orden_SQL = "SELECT cliente.codigo, cliente.nombre,

cliente.edad " + "FROM cliente ORDER BY cliente.nombre";

Statement sentencia = con.createStatement(); ResultSet rs = sentencia.executeQuery( orden_SQL );

/*** Recorrer fila a fila el resultado ****/ while ( rs.next() ) {

String res = rs.getString( "codigo" ) + ", " + rs.getString( "nombre" ) + ", " +

rs.getInt( "edad" );System.out.println( res );

} sentencia.close();}catch (SQLException e) { e.printStackTrace(); }

}

/******************************************************************* Un poco más de complejidad que el anterior Un sencillo SELECT de clientes, ordenados por nombre y especificando en WHERE una edad mínima y máxima

Page 19: Primeros Pasos Con JDBC

*******************************************************************/ void ver_cliente( int edad_min, int edad_max ) {

try { /***** Definir sentencia y ejecutarla ********/ String orden_SQL = "SELECT cliente.codigo, cliente.nombre,

cliente.edad " + "FROM cliente " + "WHERE cliente.edad BETWEEN " + edad_min

+ " AND " + edad_max + " ORDER BY cliente.nombre";

Statement sentencia = con.createStatement(); ResultSet rs = sentencia.executeQuery( orden_SQL );

/*** Recorrer fila a fila el resultado ****/ while ( rs.next() ) {

String res = rs.getString( "codigo" ) + ", " + rs.getString( "nombre" ) + ", " +

rs.getInt( "edad" );System.out.println( res );

} sentencia.close();}catch (SQLException e) { e.printStackTrace(); }

}

/******************************************************************* Un poco más de complejidad que el anterior El SELECT anterior y además con un JOIN sobre la tabla venta para ver las ventas de cada cliente. *******************************************************************/ void ver_cliente_venta( int edad_min, int edad_max ) {

try { /***** Definir sentencia y ejecutarla ********/ String orden_SQL = "SELECT cliente.codigo, cliente.nombre,

cliente.edad, " + "venta.precio, venta.coste " + "FROM cliente, venta " + "WHERE cliente.codigo = venta.codigo AND

" + "cliente.edad BETWEEN " + edad_min + "

AND " + edad_max + " ORDER BY cliente.nombre";

Statement sentencia = con.createStatement(); ResultSet rs = sentencia.executeQuery( orden_SQL );

/*** Recorrer fila a fila el resultado ****/ while ( rs.next() ) {

String res = rs.getString( "codigo" ) + ", " + rs.getString( "nombre" ) +

", " + rs.getInt( "edad" ) + ", " + rs.getDouble( "precio" ) + ", " +

rs.getDouble( "coste" );System.out.println( res );

} sentencia.close();}catch (SQLException e) { e.printStackTrace(); }

Page 20: Primeros Pasos Con JDBC

}

/******************************************************************* Un poco más de complejidad que el anterior Hago cálculos (sum) y utilizo la expresión AS. Agrupo las ventas por cliente. *******************************************************************/ void ver_beneficio_cliente() {

try { /***** Definir sentencia y ejecutarla ********/ String orden_SQL = "SELECT cliente.nombre, cliente.codigo,

sum(venta.precio) " + "AS ingresos , sum(venta.coste) AS costes,

sum(venta.precio)-sum(venta.coste) AS beneficio "+ "FROM cliente, venta WHERE cliente.codigo =

venta.codigo "+ "GROUP BY cliente.nombre";

Statement sentencia = con.createStatement(); ResultSet rs = sentencia.executeQuery( orden_SQL );

/*** Recorrer fila a fila el resultado ****/ while ( rs.next() ) {

String res = rs.getString( "nombre" ) + ", " + rs.getString( "codigo" ) +

", " + rs.getDouble( "ingresos" ) + ", " +rs.getDouble( "costes" ) + ", " +

rs.getDouble( "beneficio" ); System.out.println( res );

} sentencia.close();

} catch (SQLException e) { e.printStackTrace(); }

}}

El resultado obtenido es:

Listado de todos los clientes:230A, Ana, 3355B, Fernando, 28105A, José, 21Listado de los clientes entre 21 y 30 años:55B, Fernando, 28105A, José, 21Listado de los clientes entre 21 y 30 años con sus ventas

correspondientes:55B, Fernando, 28, 4090.0, 3450.0105A, José, 21, 3500.0, 3180.0105A, José, 21, 3600.0, 2980.0Listado de ingresos, costes y beneficio de clientes:Ana, 230A, 5070.0, 5275.0, -205.0Fernando, 55B, 4090.0, 3450.0, 640.0José, 105A, 7100.0, 6160.0, 940.0

Algunas consideraciones sobre el ejemplo:

Page 21: Primeros Pasos Con JDBC

El proceso para obtener la consulta es siempre el mismo y además es sencillo: definir sentencia, ejecutarla y realizar un bucle para recorrer fila a fila el conjunto de resultados.

ver_cliente_venta( int edad_min, int edad_max ): en esta función se listan todos los clientes cuya edad está entre edad_min y edad_max. Pero lo más interesante es que se listan las ventas de CADA cliente (recordar que un cliente puede tener N ventas). Para listar las ventas se especifica la relación que hay entre las dos tablas por medio de la clausula WHERE cliente.codigo = venta.codigo. De esta forma seleccionamos las ventas ASOCIADAS o REFERENCIADAS a su cliente correspondiente.

ver_beneficio_cliente(): en esta función hay dos aspectos de interés. Primero, se utiliza la función SUM() para realizar sumatorios. En este SELECT las columnas no son campos de la base de datos, sino operaciones (como SUM(), pero también podriamos usar AVG(), COUNT(), etc). ¿Cómo identificamos estas columnas cuando obtenemos los resultados con getXXX( nom_columna )? Para ello usamos AS nombre_columna que nos sirve para identificar o dar nombre a una columna de la consulta, por ejemplo, con sum(venta.precio)-sum(venta.coste) AS beneficio hacemos que la operación se identifique como 'beneficio' y así podemos obtener el resultado por medio de rs.getDouble( "beneficio" ). El segundo aspecto es que utilizamos GROUP BY para agrupar los sumatorios por cada cliente, es decir, obtenemos ingresos, costes y beneficio asociado a cada cliente.

Se puede observar que cerramos la sentencia y la conexión: sentencia.close() y con.close(). Otra opción es confiar en la recolección automática de basura de Java, que cierra el objeto antes de destruirlo.

Tipos de datos SQL y tipos de Java

Tengamos en cuenta que los tipos de SQL no se corresponden de forma exacta (nombre por nombre) con los de Java:

SQL Java

INT o INTEGER int

FLOAT double

DOUBLE double

CHAR(x) o VARCHAR(x) String

BOOLEAN boolean

DATE Date

TIME Time

BLOB java.sql.Blob

CLOB java.sql.Clob

Uso simultaneo de multiples sentencias (Statement)

Cuando se debe utilizar una consulta compleja, que implica numerosas subconsultas o joins, la tentación más habitual del programador es crear un objeto de la clase Statement para cada subconsulta y cruzar los datos mediante programación, no mediante una macroconsulta. Hay varias razones para no hacerlo así. Primero, hay versiones de productos que sólo admiten un Statement activo. Segundo y más importante, hazte el favor de dejar al gestor de base de datos que realize el trabajo de consultas complejas, siempre lo hará de manera más eficiente que Java.

Page 22: Primeros Pasos Con JDBC

Puede usar un objeto Statement para diversas sentencias, siempre que su ejecución sea sucesiva, no simultanea:

Connection conn = getConnection();Statement stat = conn.createStatement();stat.execute("CREATE TABLE mi_tabla (nombre CHAR(20))");stat.execute("INSERT INTO mi_tabla VALUES ('Pedro Perez')");

Pero, si va a ejecutar diversas sentencias, entonces a la hora de gestionar las excepciones debe tener en cuenta que pueden ocurrir varias excepciones. En el siguiente bucle se recogen las posibles excepciones:

catch (SQLException ex) {while (ex != null) {

ex.printStackTrace();ex = ex.getNextException();

}}

Resumen sobre la ejecución de sentencias

Resumiendo lo aprendido sobre la ejecución de sentencias:

executeQuery( String ): retorna el ResultSet de la sentencia SELECT que se le pasa como argumento. No retorna Null. Si hay un error, se gestiona a través de SQLException.

executeUpdate( String ): para sentencias que impliquen modificación de datos retorna un entero, que es el número de filas modificadas, cero si no hay cambios en filas.

execute( String ): si desea ejecutar cualquier sentencia, por ejemplo un DROP TABLE o CREATE TABLE. Retorna un boolean, cuando es true indica que la sentencia genera un ResultSet, false en caso contrario. En el siguiente ejemplo puede ver como se utilizaría (en el capítulo dedicado a metadatos tiene el ejemplo completo):

.... boolean tiene_resultados = s.execute( sentencia ); if ( tiene_resultados ) ResultSet rs = s.getResultSet( ); ....

En pos de la programación ofuscada

Que duda cabe, todo lo que se programa de forma clara es susceptible de convertirlo en algo confuso. Por ejemplo, el siguiente código es sintácticamente correcto, pero es un ejemplo de programación espaguetti (ofuscada). Este tipo de cosas son un ejemplo de solipsismo, es decir, hábitos de programación de gentes incapaces de trabajar en equipo (e incluso consigo mismo):

ResultSet rs = DriverManager.getConnection( "jdbc:mysql://localhost/prueba","root",

"palabra" ).createStatement().executeQuery( "SELECT cliente.codigo, cliente.nombre, cliente.edad " +

"FROM cliente ORDER BY cliente.nombre" );

Os lo dice una persona que tiene experiencia en equipos de desarrollo de software: los mejores programas no son los más breves (menos líneas de código), sino los más

Page 23: Primeros Pasos Con JDBC

legibles dentro de un contexto de eficiencia. Imaginemos el absurdo de un arquitecto que realiza un plano muy pequeño y fácil de transportar, pero que no puede comprender el resto del equipo (aparejador, maestro de obra, etc) ¿Es esto inteligente? Resulta evidente que la programación es una actividad colectiva, inscrita dentro de un grupo y sus métodos y procesos están destinados a favorecer la "sociabilidad" de los resultados.

Para obtener los resultados de las consultas hemos usado métodos getXXX( String nombre_columna ). Esto es bastante claro, pero que se tranquilicen todos los maniacos de la confusión, podemos hacerlo un poco menos claro, obteniendo los resultados por medio de funciones getXXX( int numero_columna ), donde el argumento indica el orden de columna dentro de la consulta (empezando desde uno, no desde cero).

Las funciones getXXX( int numero_columna ) no tienen mucho sentido cuando el programador conoce la estructura y relaciones de los datos. Sin embargo, en ocasiones hay que programar aplicaciones que accedan a diversas bases de datos y/o acceder a bases de datos desconocidas. En este contexto tiene sentido el uso de dichas funciones. También resultan útiles para obtener metadatos, como veremos en el capítulo dedicado a los metadatos.

Volver al índice

Servlets y JDBC

(Octubre de 2005, actualizado en Abril de 2007)

Introducción

Vamos a tratar una serie de aspectos de interés:

1. Los servlets hacen en init() la carga del driver y en doPos()/doGet se conectan a la base de datos y ejecutan una consulta que se escribe sobre la página.

2. Practicamos el patrón DAO: los servlets no acceden directamente a la base de datos; sino que usan clases DAO que por medio de JDBC acceden a la base de datos. En un ejemplo anterior hemos tratado el patrón DAO. En este caso usamos como base el mismo código del ejemplo de patrón DAO, pero introduciendo ligeras diferencias (permitimos que cada conexión se haga con un login/password diferente).

3. Vamos a "anidar" servlets, dicho de otro modo: un servlet tiene como salida un formulario, que invoca a otro servlet. El primer servlet imprime un combobox ('desplegable') con todos los clientes, dando la opción al usuario de seleccionar uno. El segundo muestra las ventas del cliente seleccionado.

4. Además vamos a manejar la sesión. Es necesario que las llamadas a los servlets mantengan cierta continuidad, como ocurre en una web comercial, donde pasamos por

Page 24: Primeros Pasos Con JDBC

las páginas del catálogo y se mantiene información del carrito de la compra o de cliente. Para mantener información a lo largo de sucesivas llamadas necesitamos usar atributos de sesión.

5. Hay que tener en cuenta la estructura del ejemplo. El directorio raíz es docen_servlet01.JDBC01 (de la aplicación /public_html), del que parten los paquetes (directorios):

1. bean. Las clases (Cliente y Venta) que representan las entidades (tablas) de la base de datos. Todas ellas implementan el interface Bean.

2. accesoDatos. Las clase DAO (DAOCliente y DAOVenta) que consultan (select) la base de datos, implementan el interface InterfaceDAO y heredan de DAOGeneral (servicios comunes de carga de driver, conexión, desconexión, etc.)

3. presentacion. Los servlets (FormClientes y FormVentas) con una clase de utilidad general (UtilGeneral).

Sólo los servlets (FormClientes y FormVentas) deben estar indicados en web.xml.

Los servlets en web.xml:

.... &ltservlet> &ltservlet-name>FormClientes</servlet-name>

Page 25: Primeros Pasos Con JDBC

&ltservlet-class>docen_servlet01.JDBC01.presentacion.FormClientes</servlet-class> </servlet> &ltservlet> &ltservlet-name>FormVentas</servlet-name> &ltservlet-class>docen_servlet01.JDBC01.presentacion.FormVentas</servlet-class> </servlet> &ltservlet-mapping> &ltservlet-name>FormClientes</servlet-name> &lturl-pattern>/servlet/FormClientes</url-pattern> </servlet-mapping> &ltservlet-mapping> &ltservlet-name>FormVentas</servlet-name> &lturl-pattern>/servlet/FormVentas</url-pattern> </servlet-mapping> ....

Formulario de ejemplo que muestra los clientes y sus ventas, anidando servlets:

Usuario:  

Password:

Pede obtener el código del ejemplo.

El ejemplo: los clientes y sus pedidos

En nuestra base de datos tenemos dos tablas: la primera representa información de clientes y la segunda contiene las ventas que se han realizado. La relación es de 1:N, ya que un cliente puede tener asignadas varias ventas. En el ejemplo:

1. El primer servlet realiza un formulario con una lista de clientes. 2. El segundo servlet recibe el cliente seleccionado y consulta en la base de datos las

ventas de dicho cliente. El resultado es una página con las ventas del cliente.

proactiv_alumnos

*******

Entrar

Page 26: Primeros Pasos Con JDBC

Esquema global (nota: el servlet 'inicio' del dibujo se llama en realidad 'FormClientes' y el servlet 'ventas' se llama 'FormVentas'):

El servlet FormClientes

FormClientes empieza iniciando el DAO:

package docen_servlet01.JDBC01.presentacion;import ... public class FormClientes extends HttpServlet {

private DAOCliente dc = null; // Clase responsable de acceso a base de datos

/************************************************************************

Al inicializarse el servlet se crea el DAO: con este se carga el driver JDBC y

se leen propiedades. El argumento del constructor del DAO es el dir raiz de la

aplicación. Si ya se hubiesen cargado driver y propiedades, no se vuelven

a cargar.

**************************************************************************/

public void init(ServletConfig config) throws ServletException {

super.init(config);dc = new

DAOCliente( config.getServletContext().getRealPath("/") );}....

A continuación veamos doPost(). Puede observarse en el código fuente que las salidas de código HTML se envian a la clase auxiliar UtilGeneral. Empezamos comprobando que el DAO ha cargado las propiedades y el driver de base de datos, las propiedades se definen en un archivo properties, que define el host, y dicho archivo se sitúa (desde el directorio raíz de la aplicación, public_html) en public_html/propiedades/docen_servlet01/parametros.properties. Pero esta lectura de archivo properties queda oculta (encapsulada) para el servlet, de ello se encarga la clase Propiedades, que es usada por el DAO:

public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

PrintWriter out= response.getWriter(); // Obtener flujo salida;

try {

Page 27: Primeros Pasos Con JDBC

HttpSession sesion = request.getSession(true); // Obtener sesión, si no existe, la crea

response.setContentType("text/html; charset=iso-8859-1"); // Definir tipo de salida

UtilGeneral.imprimirInicioPagina( "Ejemplo de servlet", "Seleccione cliente", out);

//// Si no se han cargado propiedades, aviso y salgo

if ( dc.getPropiedades() == null ) {UtilGeneral.imprimir( out, "No se han

cargado las propiedades.");return;

}else

UtilGeneral.imprimir( out, "Propiedades cargadas: " +

dc.getPropiedades().getPathPropiedades()+dc.getPropiedades().getFicheroParametros());

//// Si no he podido cargar el driver JDBC, aviso y salgo

if ( !dc.estaCargadoDriver() ) {UtilGeneral.imprimir( out, "No se ha

cargado el driver " + DAOGeneral.getPropiedades().getDriver(), true, true );

UtilGeneral.imprimirFinPagina( out );return;

}else

UtilGeneral.imprimir( out, "Driver cargado: " + dc.getPropiedades().getDriver());

....

En el archivo properties (/public_html/propiedades/docen_servlet01/parametros.properties) tenemos las siguientes líneas que definen el host y la base de datos:

basedatos.host=jdbc:mysql://localhost:3306/docen_servlet01.JDBC01.basedatos=proactiv_prueba

Lo que sigue es definir los atributos de la sesión y mostrar el formulario que lista los clientes de la base de datos. La definición de atributos es sencilla. El login y password se obtienen de la petición (request) y la base de datos se obtiene de la clase Propiedades que usa DAOCliente. El formulario se escribe mediante una llamada al método del propio servlet imprimirFormulario()

....//// Poner atributos (base de datos, login y

password en la sesion)//// La base de datos se obtiene de un archivo

properties y //// el login y password de requestsesion.setAttribute("basedatos",

dc.getPropiedades().getHostBaseDatos()+

Page 28: Primeros Pasos Con JDBC

dc.getPropiedades().getParametro("docen_servlet01.JDBC01.basedatos"));sesion.setAttribute("login",

request.getParameter("login"));sesion.setAttribute("password",

request.getParameter("password"));

////////////////////////// IMPRIMIR FORMULARIO Y SESION

imprimirFormulario( request, out );UtilGeneral.imprimirSesion(sesion, out);

}catch (Exception e) {

UtilGeneral.imprimir( out, "Error general. " + e.getMessage(), true, true );

e.printStackTrace();}finally {

//// Cierre de páginaUtilGeneral.imprimirFinPagina( out );

}}

imprimirFormulario() empieza con con dos llamadas a DAOCliente. Primero para definir el usuario (setIdentificacion()) a partir del login y password de la petición (request) HTTP y en segundo lugar obtener un vector de clientes (bean.Cliente) por medio de la llamada dc.select(null). EL argumento null indica que queremos listar todos los clientes, es decir, que no hay clausula WHERE en la sentencia SQL del DAO:

void imprimirFormulario( HttpServletRequest request, PrintWriter out ) {

try {Vector vecClientes = null;

//// Almacenar en DAO la identificación (login-pwd), desde request

dc.setIdentificacion(request.getParameter("login"), request.getParameter("password"));

//// Usar el DAO para conseguir vector de clientes

try {vecClientes = dc.select(null);

}catch ( Exception e) {

UtilGeneral.imprimir(out, "Error en la consulta. " + e.getMessage());

}....

A continuación imprimirFormulario() escribe en la salida el código HTML. Escribe los dos 'desplegables' que pueden verse en el formulario. Uno tiene los nombres de los clientes y otro tiene sus códigos.

Page 29: Primeros Pasos Con JDBC

///// Inicio de tabla HTMLout.println("&lttable BORDER=1 align=center

cellpadding='10' cellspacing=1>");out.println("&lttr>&lttd bgcolor=#00FF00>");out.println("&ltFONT color=#000080

FACE='Arial,Helvetica,Times' SIZE=2>");

///// Inicio del formulario HTMLout.println("&ltform action="+

dc.getPropiedades().getHostHTTP()+"servlet/FormVentas method='post'>");

UtilGeneral.imprimir( out, "Escoja cliente:");

///// Recorrer fila a fila el vector de clientes y poner en SELECT de NOMBRES

out.println("&ltSELECT NAME='cliente' onchange=\"copiarValor('nombre','codigo');\" id='nombre'>");

for ( int i = 0; i< vecClientes.size(); i++ ) {Cliente c = (Cliente)

vecClientes.get(i);out.println("&ltOPTION VALUE=" +

c.getCodigo()+">" + c.getApe1() + " " + c.getApe2() + ", " + c.getNombre()+"");

}out.println("</SELECT>");

///// Recorrer fila a fila el vector de clientes y poner en SELECT de CODIGOS

out.println("&ltSELECT NAME='cliente.codigo' onchange=\"copiarValor('codigo','nombre');\" id='codigo'>");

for ( int i = 0; i< vecClientes.size(); i++ ) {Cliente c = (Cliente)

vecClientes.get(i);out.println("&ltOPTION VALUE=" +

c.getCodigo()+">" + c.getCodigo()+"</OPTION>");}out.println("</SELECT>");

//// Alternativa: campo oculto para el código de cliente (en vez de SELECT de codigos)// out.println("&ltINPUT TYPE=HIDDEN NAME='cliente.codigo' id='codigo'>");

//// Poner botón y fin de formulario y de tablaUtilGeneral.imprimir( out, "&ltinput

type='submit' name='Submit' value='Enviar'>");

out.println("</form></font></td></tr></table>");

}catch (Exception e) {

UtilGeneral.imprimir( out, "EROR EN FORMULARIO. " + e.getMessage(), true, true );

}}

Un aspecto a resaltar de los dos 'desplegables' es que están coordinados: si cambia en uno el nombre del cliente, entonces cambia en el otro a su correspondiente código de cliente y viceversa. Esto se consigue gracias a que en UtilGeneral.imprimirInicioPagina() tenemos la siguiente función javascript:

Page 30: Primeros Pasos Con JDBC

&ltscript type='text/javascript'>function copiarValor(idOrigen, idDestino) {

document.getElementById(idDestino).value = document.getElementById(idOrigen).value;

}</script>

Hay una alternativa, la forma más común de trabajar es tener un 'desplegable' para los nombres de cliente y un campo de texto oculto que refleja el código del nombre seleccionado en el 'desplegable'. Lo ocultamos por una simple razón: al usuario sólo le interesa ver los nombres de los clientes y su clave primaria (el código de cliente) suele resultarle indiferente (a menos que dicho código sea significativo, como por ejemplo el NIF). Para ocultar el código se puede probar a sustituir el 'desplegable' (select) de códigos por:

//// Alternativa: campo oculto para el código de cliente (en vez de SELECT de codigos)// out.println("&ltINPUT TYPE=HIDDEN NAME='cliente.codigo' id='codigo'>");

Se queda oculto (hidden) y no borrado, ya que este campo será el que se transmita en la petición (request) al segundo servlet (FormVentas), pues dicho campo contiene el código del cliente del que queremos mostrar las ventas.

Transferencia de datos (session y request)

Pensemos en el paso de información del primer al segundo servlet. Analizando:

En la sesión se transporta la base de datos, login y password. Estos datos serán leidos por el segundo servlet.

En la request se transporta el código de cliente. Se ha podido ver que el segundo 'desplegable' tiene como 'name' 'cliente.codigo', este es el nombre de parámetro que usara el segundo servlet (FormVentas) para obtener el código de cliente seleccionado. Concretamente por medio de: request.getParameter("cliente.codigo").

Obteniendo la información de sesion (método imprimirSesion() de UtilGeneral)

Por medio de un iterador (Enumeration) obtenemos todos los atributos de la sesión:

static void imprimirSesion(HttpSession sesion, PrintWriter out) {

if (sesion != null) {out.println("&ltP>&ltB>Sesion:</B>" +

sesion.getId() + ". Atributos:" + "&ltOL>");for (Enumeration e =

sesion.getAttributeNames(); e.hasMoreElements(); ) {String atrib = (String) e.nextElement();out.print("&ltLI>Nombre: " + atrib);out.println(". Valor: " +

sesion.getAttribute(atrib) + "</LI>");

Page 31: Primeros Pasos Con JDBC

}out.println("</OL>");

}}

El aspecto más importante es que obtenemos el valor de un atributo por medio de:

sesion.getAttribute(atrib);

que devuelve un objeto del tipo Object. Podemos conseguir todos los atributos de una sesión, a partir de una enumeración devuelta por sesion.getAttributeNames() (de la misma forma que obteniamos todos los parámetros por medio de request.getParameterNames()):

FormVentas

El servlet que debe obtener las ventas de un cliente seleccionado recibe información por dos medios:

1. Recibe el código de cliente seleccionado como un parámetro de request.2. Recibe como atributos de la sesión el nombre de la base de datos, login y password.

El procesamiento de la respuesta en el servlet 'FormVentas' es semejante al del servlet anterior, por ello no vamos a reincidir con detalles reiterados; como por ejemplo que se usa un DAO (DAOVenta). En FormVentas.imprimirFormulario() primero se envía al DAO la identificación (login y password) del usuario por medio de dv.setIdentificacion() y en segundo lugar se obtiene un vector de elementos de la clase Venta, por medio de una llamada a:

vecVentas = dv.select( "codigo = '" + request.getParameter("cliente.codigo")+"'");

El argumento de select() indica la cláusula WHERE.

Para que el formateo de la tabla de ventas sea correcto usamos un formateador de números para mostrar los separadores de millares y de decimales.

DecimalFormat myFormatter = new DecimalFormat( "##,###,###.#");myFormatter.setMinimumFractionDigits(1);....out.println("&ltTD align=right>" + myFormatter.format( precio )

+ "</TD>");out.println("&ltTD align=right>" + myFormatter.format( coste )

+ "</TD>");out.println("&ltTD align=right>" + myFormatter.format( precio -

coste )+ "</TD>");   

El código completo de FormVentas es:

package docen_servlet01.JDBC01.presentacion;

import javax.servlet.ServletException;

Page 32: Primeros Pasos Con JDBC

import javax.servlet.http.*;import javax.servlet.ServletConfig;import java.io.PrintWriter;import java.io.IOException;import java.util.Vector;import java.text.DecimalFormat;

import docen_servlet01.JDBC01.accesoDatos.DAOVenta;import docen_servlet01.JDBC01.presentacion.UtilGeneral;import docen_servlet01.JDBC01.bean.Venta;

/*************************************************************************** * Recibe el cliente (parámetro de formulario). Ejecuta una consulta de las * ventas de dicho cliente. Los datos son impresos en la página HTML. * Utiliza el DAOVenta para el acceso a la base de datos. ******************************************************************************/public class FormVentas extends HttpServlet {

private DAOVenta dv = null;

/************************************************************************

Al inicializarse el servlet se crea el DAO: en éste se carga el driver JDBC y se leen

propiedades. El argumento del constructor del DAO es el path de la aplicación.

Si ya se hubiesen cargado driver y propiedades, no se vuelven a cargar.

*************************************************************************/

public void init(ServletConfig config) throws ServletException {

super.init(config);dv = new

DAOVenta( config.getServletContext().getRealPath("/") );}

/***************************************************************** * Procesar una petición HTTP con el método POST * Muestro las ventas del cliente que se pasa como argumento

del formulario

*****************************************************************/public void doPost(HttpServletRequest request,

HttpServletResponse response) throws ServletException, IOException {PrintWriter out= response.getWriter(); //

Obtener flujo salida;try {

HttpSession sesion = request.getSession( false ); // Obtener sesión

response.setContentType("text/html; charset=iso-8859-1"); // Definir tipo de salida

Page 33: Primeros Pasos Con JDBC

UtilGeneral.imprimirInicioPagina( "Ejemplo de servlet", "Ventas del cliente seleccionado", out);

//// Si hay sesión, mostrar atributos y resto de página

if ( sesion != null) {UtilGeneral.imprimirSesion(sesion, out);imprimirFormulario( request, out ); //

Imprimir salida}else

UtilGeneral.imprimir(out, "La sesión no está disponible", true, true);

}catch (Exception e) {

UtilGeneral.imprimir( out, "Error general. " + e.getMessage(), true, true );

}finally {

UtilGeneral.imprimirFinPagina( out );}

}

/***************************************************************** * Procesar una petición HTTP con el método GET. Reenvia a

doPost

*****************************************************************/public void doGet( HttpServletRequest request,

HttpServletResponse response) throws ServletException, IOException {doPost(request, response);

}

/************************************************************************

Imprimo tabla de ventas

**************************************************************************/

void imprimirFormulario( HttpServletRequest request, PrintWriter out ) {

try {Vector vecVentas = null;HttpSession sesion = request.getSession(false);

// Obtener sesión

//// DAO: asignar identificación (login-pwd)

dv.setIdentificacion( (String)sesion.getAttribute("login"), (String)sesion.getAttribute("password"));

//// Usar el DAO para conseguir vector de clientes. Argumento: el código de cliente

try {vecVentas = dv.select( "codigo = '" +

request.getParameter("cliente.codigo")+"'");}catch ( Exception e) {

Page 34: Primeros Pasos Con JDBC

UtilGeneral.imprimir(out, "Error en la consulta. " + e.getMessage());

}

//// Inicio de tablaout.println("&ltP>Ventas al cliente " +

request.getParameter( "cliente" ) + ":");out.println("&ltTABLE BORDER=1

align='center'>");out.println("&ltTR bgcolor=#00CCFF>");out.println("&ltTH>CODIGO</TH>");out.println("&ltTH>PRECIO</TH>");out.println("&ltTH>COSTE</TH>");out.println("&ltTH>BENEFICIO</TH>");out.println("</TR>");

//// Formateador de númerosDecimalFormat myFormatter = new

DecimalFormat( "##,###,###.#");myFormatter.setMinimumFractionDigits(1);

if ( vecVentas.size() == 0)UtilGeneral.imprimir( out, "No hay

ventas registradas", true, true );

///// Recorrer fila a fila y poner en tablafor ( int i = 0; i < vecVentas.size(); i++ ) {

Venta v = (Venta) vecVentas.get(i);

out.println("&ltTR bgcolor=#00FF00>");out.println("&ltTD>" + v.getCodigo() +

"</TD>");float precio =

v.getPrecio().floatValue();float coste = v.getCoste().floatValue();out.println("&ltTD align=right>" +

myFormatter.format( precio ) + "</TD>");out.println("&ltTD align=right>" +

myFormatter.format( coste ) + "</TD>");out.println("&ltTD align=right>" +

myFormatter.format( precio - coste )+ "</TD>");out.println("</TR>");

}out.println("</TABLE>");

}catch (Exception e) {

UtilGeneral.imprimir( out, "EROR EN FORMULARIO. " + e.getMessage(), true, true );

}}

}

Volver al índice

JDBC: Inserción y actualización

Page 35: Primeros Pasos Con JDBC

Después de haber aprendido lo sencillo que es implementar una sentencia SELECT vamos a estudiar INSERT Y UPDATE.

INSERT

La diferencia más importante respecto a SELECT es que usamos la función executeUpdate(). En el siguiente ejemplo damos por supuesto que se han creado previamente los objetos de la clase Connection (con) y Statement (sentencia):

int add_cliente( String codigo, String nombre, String ape1, String ape2, int edad ) {

try {String orden_SQL = "INSERT INTO CLIENTE VALUES

" + "('" + codigo +"', '" + nombre + "', '" + ape1

+ "', '" + ape2 + "', " + edad + ")";

Statement sentencia = con.createStatement();int fila = sentencia.executeUpdate(orden_SQL);sentencia.close();return fila;

}catch (SQLException e) {

e.printStackTrace();return 0;

}}

executeUpdate() devuelve el número de filas afectadas. Evidentemente, en nuestro ejemplo con INSERT si se produce la inserción devuelve uno y cero en caso de error. Conviene recordar que 'cliente.codigo' está definida como clave primaria (si hay intentos de insertar codigos repetidos se desencadena una excepción)

En este ejemplo se han insertado todos los valores en el orden en que están definidos los campos de la tabla. Podemos hacerlo de otra forma para insertar un registro que sólo tenga definido el campo 'cliente.codigo':

INSERT INTO cliente SET codigo = '31JK'

UPDATE

En el caso de UPDATE vamos a realizar un ejemplo en el que incrementamos el valor de las ventas un 10%:

....update_venta( 1.1 );....

/***** UPDATE de registros, devuelve el número de registros actualizados ****/

int update_venta( double incremento ) {try {

String orden_SQL = "UPDATE venta SET precio = precio * " + incremento;

Page 36: Primeros Pasos Con JDBC

Statement sentencia = con.createStatement();int fila = sentencia.executeUpdate(orden_SQL);sentencia.close();return fila;

}catch (SQLException e) {

e.printStackTrace();return 0;

}}

Si deseasemos realizar una actualización selectiva (por ejemplo, para las ventas mayores de 4500), tendremos que utilizar la clausula WHERE.

Volver al índice

Sentencias preparadas

Introducción

Supongamos que hay un tipo de sentencia que se repite con frecuencia. En nuestro ejemplo podemos necesitar un informe de los clientes en función de su edad:

select nombre,edad from cliente where edad >= 18 and edad <= 25 select nombre,edad from cliente where edad >= 20 and edad <= 30

El problema de esto es que para todas y cada una de estas sentencias el gestor de bases de datos debe analizar y compilar. Esto supone un consumo innecesario de recursos. Podriamos tener preparado un esquema de sentencia y parametrizarlo en función de las necesiades del usuario. En resumen, las sentencias preparadas resultan más eficientes y más cómodas.

Un ejemplo

En este ejemplo vamos a preparar un esquema de sentencia que es parametrizable, para obtener los clientes en función de su edad. Los parámetros se indican con un interrogante (?):

//// Sentencia preparada PreparedStatement st = con.prepareStatement( "select

nombre,edad from cliente where edad >= ? and edad <=?");

//// El usuario nos da la edad minima y la máxima BufferedReader entrada = new BufferedReader(new

InputStreamReader(System.in)); System.out.print( "Edad mínima: "); int edadMin = Integer.parseInt( entrada.readLine() ); System.out.print( "Edad máxima: "); int edadMax = Integer.parseInt( entrada.readLine() );

//// Pongo los parámetros de la sentencia preparada (edad mínima y máxima)

st.setInt(1, edadMin ); st.setInt(2, edadMax );

Page 37: Primeros Pasos Con JDBC

//// Ejecuto sentencia y recorro el conjunto de resultados ResultSet rs = st.executeQuery(); while ( rs.next() )

System.out.println( "Nombre: " + rs.getString( "nombre" ) + ", Edad: " + rs.getInt( "edad" ) );

st.close();

Con el primer argumento de setInt() indicamos el número de parámetro que estamos definiendo: el primero y el segundo

Volver al índice

METADATOS

¿Meta qué?

Sabemos que es un dato (por ejemplo, el número 32 o la palabra "Juan"). ¿Qué es un metadato? Cualquier información al respecto del dato que nos ayuda a comprenderlo o utilizarlo. Los primeros metadatos de interés son aquellos que tienen que ver con la estructura de datos, como el nombre y tipo de una columna, por ejemplo, el 32 es uno de los datos de la columna 'edad', del tipo int, que está en la tabla 'cliente'. Pero esto es trivial, metadatos propiamente dichos son información sobre el origen de datos de la columna o tabla, una explicación de la columna o tabla, restricciones, la descripción del cálculo al que obedece, etc. Por ejemplo, la columna 'edad' puede tener como metadato una descripción del modo en que se ha calculado: 'fecha_actual - tabla1.fecha_nacimiento'.

El manejo de metadatos en JDBC tiene interés cuando tenemos que realizar aplicaciones que acceden a cualquier base de datos o ejecuten cualquier sentencia dada por el usuario.

Ejecutar cualquier consulta (query)

Vamos a realizar una sencilla función que ejecuta cualquier SELECT que el usuario introduzca por teclado.En nuestro ejemplo tenemos que observar:

Mediante una llamada a nuestro método String obt_sentencia() obtenemos por medio de teclado la sentencia SQL:

static String obt_sentencia() { try { /* Creo el objeto 'entrada', es un lector de

entradas por teclado */ BufferedReader entrada = new BufferedReader(new

InputStreamReader(System.in)); /****** Pido la sentencia por teclado ******/ System.out.println( "Teclee consulta:"); return entrada.readLine(); }

Page 38: Primeros Pasos Con JDBC

catch (IOException e) { e.printStackTrace(); return null; }

}

Creamos una sentencia (Statement): Statement s = con.createStatement();

Ejecutamos la sentencia execute() que devuelve true si el resultado de la sentencia es un ResultSet, como ocurre con SELECT.

boolean tiene_resultados = s.execute( sentencia );

Si hay un ResultSet, entonces ejecutamos la función ver_resultados( Statement ) de la clase consul_x:

if ( tiene_resultados ) (new consulta_x( con )).ver_resultados( s );

En la función ver_resultados( Statement ) usamos los metadatos para obtener el número de columnas:

ResultSet rs = s.getResultSet( ); ResultSetMetaData mdata = rs.getMetaData(); //

Obtener Metadatos int num_columnas = mdata.getColumnCount(); //

Obtener número de columnas

Después obtenemos los nombres de las columnas:

for ( int i = 1; i <= num_columnas; i++ ) {if ( i > 1 )

System.out.print( ", " );System.out.print(

mdata.getColumnLabel( i ) ); // Mostrar nombres de campos}

El ejemplo, con el código fuente completo es:

package jdbc01;import java.sql.DriverManager;import java.sql.Connection;import java.sql.Statement;import java.sql.ResultSet;import java.sql.ResultSetMetaData;import java.sql.SQLException;import java.lang.ClassNotFoundException;import java.io.*;

Page 39: Primeros Pasos Con JDBC

public class metadatos {public static void main(String[] args) {

Connection con = null;

try {/*** Registro de driver ****/Class.forName("com.mysql.jdbc.Driver");

/*** Crear conexión con base de datos ***/con =

DriverManager.getConnection( "jdbc:mysql://localhost/prueba", "root", "palabra" );

/*** Obtiene la sentencia (String) por teclado ***/

String sentencia = obt_sentencia();

/*** Crea un objeto Statement ***/Statement s = con.createStatement();

/*** Ejecutamos la sentencia: si hay resultados, los mostramos ***/

boolean tiene_resultados = s.execute( sentencia );

if ( tiene_resultados )(new

consulta_x( con )).ver_resultados( s ); // La clase consul_x obtiene metadatos y datos

s.close();}catch( ClassNotFoundException e )

{ e.printStackTrace(); }catch (SQLException e) { e.printStackTrace(); }

/*** Haya excepción o no, tengo que cerrar la conexión ***/

finally {cerrar_conexion( con );

}}

static String obt_sentencia() {try {

/* Creo el objeto 'entrada', es un lector de entradas por teclado */

BufferedReader entrada = new BufferedReader(new InputStreamReader(System.in));

/****** Pido la sentencia por teclado ******/System.out.println( "Teclee consulta:");

return entrada.readLine();}catch (IOException e) { e.printStackTrace(); return

null; }}

/*************************************************************Me aseguro de que se cierra la conexión*************************************************************/

Page 40: Primeros Pasos Con JDBC

public static void cerrar_conexion( Connection con ) {try {

if ( con != null )if ( !con.isClosed() ) // Si no está

cerrada, la cierrocon.close();

}catch (SQLException e) { e.printStackTrace(); }

}

}

class consulta_x {Connection con;consulta_x( Connection con ) {

this.con = con;}

/*** Muestra los resultados de cualquier sentencia (query) ***/void ver_resultados( Statement s ) {

try {ResultSet rs = s.getResultSet( );ResultSetMetaData mdata = rs.getMetaData(); //

Obtener Metadatosint num_columnas = mdata.getColumnCount(); //

Obtener número de columnas

/*** Mostrar nombres de columnas ****/for ( int i = 1; i <= num_columnas; i++ ) {

if ( i > 1 )System.out.print( ", " );

System.out.print( mdata.getColumnLabel( i ) ); // Mostrar nombres de campos

}

/*** Mostrar resultados. Fila a fila. Dentro de cada fila: columna a columna ****/

while (rs.next()) {System.out.println( "" );

// Nueva línea antes de la filafor ( int i = 1; i <= num_columnas; i++)

{ // Recorro las columnasif ( i > 1 )

System.out.print( ", " );

System.out.print( rs.getString(i) );}

}}catch (SQLException e) { e.printStackTrace(); }

}}

Además podemos obtener el tamaño de cada columna por medio de:

int size = mdata.getColumnDisplaySize( int numero_columna );

Obtener la estructura y prestaciones de la base de datos

Page 41: Primeros Pasos Con JDBC

Antes vimos que el manejo de metadatos en JDBC tiene interés cuando se tienen que ejecutar sentencias de las que se desconoce su estructura a priori. En este apartado vamos a ir a más: pudiera ocurrir que necesitemos acceder a información de la base de datos, tablas o incluso de las capacidades de nuestro controlador JDBC.

Para comprender el tipo de metadato que podemos conseguir, vamos a empezar por el final (por el resultado), mostrando los metadatos que obtiene nuestra aplicación, simplemente partiendo de la información de driver y base de datos. Lo que muestra la aplicación se dividide en dos bloques:

El primer bloque de información es general: características del gestor de bases de datos, driver JDB, etc.

El segundo da información específica a cada tabla: nombre, columnas, tipo de columnas, etc.

--------- CARACTERISTICAS GENERALES ---------------Nombre de BD: MySQLVersión de BD: 4.0.18-max-debugNombre de controlador: MySQL-AB JDBC DriverVersión de controlador: mysql-connector-java-3.0.11-stable ( $Date: 2004/02/04 02:47:36 $, $Revision: 1.27.2.34 $ )Versión mayor de controlador JDBC: 3Versión menor de controlador JDBC: 0Statements simultaneos: 0Soporte SQL92: falseSoporte de SCROLL_SENSITIVE: falseAdemás actualiza BD: false--------- CARACTERISTICAS DE CADA TABLA ------------TABLE_CAT: prueba, TABLE_SCHEM: null, TABLE_NAME: cliente, TABLE_TYPE: TABLE, REMARKS:

codigo (Tipo Java:java.lang.String) (Tipo SQL:CHAR) (Size=10)nombre (Tipo Java:java.lang.String) (Tipo SQL:CHAR) (Size=20)ape1 (Tipo Java:java.lang.String) (Tipo SQL:CHAR) (Size=20)ape2 (Tipo Java:java.lang.String) (Tipo SQL:CHAR) (Size=20)edad (Tipo Java:java.lang.Integer) (Tipo SQL:LONG) (Size=11)

TABLE_CAT: prueba, TABLE_SCHEM: null, TABLE_NAME: venta, TABLE_TYPE: TABLE, REMARKS:

codigo (Tipo Java:java.lang.String) (Tipo SQL:CHAR) (Size=10)precio (Tipo Java:java.lang.Float) (Tipo SQL:FLOAT) (Size=12)coste (Tipo Java:java.lang.Float) (Tipo SQL:FLOAT) (Size=12)

En el bloque de información general es importante destacar las prestaciones:

Statements simultaneos: cuantos statement se pueden mantener abiertos de manera simultanea

Soporte SQL92: si tiene soporte completo para el estándar SQL 92 Soporte de SCROLL_SENSITIVE: si los ResultSet tienen desplazamiento (scroll) y son

sensibles a los cambios en la base de datos. Además actualiza BD: si además de lo anterior el ResultSet puede usarse para

actualizar la base de datos.

En el bloque de información de cada tabla cabe destacar::

TABLE_CAT: nombre de la base de datos. TABLE_TYPE: tipo (TABLE, VIEW, ALIAS, etc.). Para cada columna observe la correspondencia de tipos de datos SQL y Java.

El dódigo fuente completo y comentado es:

Page 42: Primeros Pasos Con JDBC

package jdbc01;import java.sql.DriverManager;import java.sql.Connection;import java.sql.Statement;import java.sql.DatabaseMetaData;import java.sql.ResultSet;import java.sql.ResultSetMetaData;import java.sql.SQLException;import java.lang.ClassNotFoundException;

public class metadatos2 { public static void main(String[] args) {

Connection con = null;try { /* Registro de driver **/ Class.forName("com.mysql.jdbc.Driver");

/* Crear conexión con base de datos */ con = DriverManager.getConnection(

"jdbc:mysql://localhost/prueba", "root", "palabra" );

/* Obtener metadatos de base de datos */ DatabaseMetaData dbmd = con.getMetaData();

/********** Obtengo el ResultSet de las tablas: El tercer argumento puede especificar el nombre de la

tabla El cuarto argumento especifica los tipos (TABLE, VIEW,

etc.). null: todos *********/ ResultSet rs = dbmd.getTables( null, null, null, new

String[]{"TABLE"} );

/**** Obtenemos el ResultSetMetaData a partir del anterior, además obt. núm. columnas ****/

ResultSetMetaData rsmd = rs.getMetaData(); int num_cols = rsmd.getColumnCount();

/**** Mostrar información general *****/ System.out.println( "--------- CARACTERISTICAS GENERALES

---------------" ); System.out.println( "Nombre de BD: " +

dbmd.getDatabaseProductName() ); System.out.println( "Versión de BD: " +

dbmd.getDatabaseProductVersion() ); System.out.println( "Nombre de controlador: " +

dbmd.getDriverName() ); System.out.println( "Versión de controlador: " +

dbmd.getDriverVersion() ); System.out.println( "Versión mayor de controlador JDBC: " +

dbmd.getJDBCMajorVersion() ); System.out.println( "Versión menor de controlador JDBC: " +

dbmd.getJDBCMinorVersion() ); System.out.println( "Statements simultaneos: " +

dbmd.getMaxStatements() ); System.out.println( "Soporte SQL92: " +

dbmd.supportsANSI92FullSQL() ); System.out.println( "Soporte de SCROLL_SENSITIVE: " +

Page 43: Primeros Pasos Con JDBC

dbmd.supportsResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE)); System.out.println( "Además actualiza BD: " +

dbmd.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE));

System.out.println( "--------- CARACTERISTICAS DE CADA TABLA ------------" );

/****************** ResultSet para tratar cada tabla: la fila tiene las

características de la tabla (base de datos, esquema, nombre, tipo y

descripción) *****************/ while ( rs.next() ) {

/*** Recorremos las características de cada tabla ***/for ( int i = 1; i <= num_cols; i++ ) { if ( i > 1 )

System.out.print( ", " ); System.out.print( rsmd.getColumnLabel(i) + ":

"); // Nombre de campo System.out.print( rs.getString(i) ); // Valor

de campo

} /// Fin del recorrido por las características de cada tabla

/*********** Mostrar columnas de la tabla *************/

mostrar_cols_tabla( con, rs.getString(3) ); // getString(3)==nombre_tabla

System.out.println( "" ); // Nueva línea después de terminar con tabla

} //// Fin del tratamiento de cada tabla

rs.close();}catch( ClassNotFoundException e ) { e.printStackTrace(); }catch (SQLException e) { e.printStackTrace(); }/*** Haya excepción o no, tengo que cerrar la conexión ***/finally {

cerrar_conexion( con );}

}

/*************************************************************Me aseguro de que se cierra la conexión*************************************************************/public static void cerrar_conexion( Connection con ) {

try {if ( con != null )

if ( !con.isClosed() ) // Si no está cerrada, la cierro

con.close();}

Page 44: Primeros Pasos Con JDBC

catch (SQLException e) { e.printStackTrace(); }}

/********* Muestra campos (columnas) de la tabla (String nom_tabla) ********/ static void mostrar_cols_tabla( Connection con, String nom_tabla ) {

try { /*** Ejecutar sentencia SELECT para la tabla ****/ Statement senten = con.createStatement(); ResultSet rs_sel = senten.executeQuery("SELECT * FROM " +

nom_tabla );

/*** Obtenemos metadatos de la sentencia SELECT ***/ ResultSetMetaData mdata_sel = rs_sel.getMetaData(); //

Obtener Metadatos de consulta int num_cols_sel = mdata_sel.getColumnCount(); // Obtener

número de columnas

/*** Recorremos cada columna para mostrar sus características ****/

for (int num_col_sel = 1; num_col_sel <= num_cols_sel; num_col_sel++) {

System.out.println("");/* Mostrar nombre de columna */System.out.print("\t" +

mdata_sel.getColumnLabel(num_col_sel));/* Mostrar tipo Java */System.out.print(" (Tipo Java:" +

mdata_sel.getColumnClassName(num_col_sel) +")");/* Mostrar tipo SQL */System.out.print(" (Tipo SQL:" +

mdata_sel.getColumnTypeName(num_col_sel) +")");/* Mostrar tamaño */System.out.print(" (Size=" +

mdata_sel.getColumnDisplaySize(num_col_sel) +")"); } senten.close(); rs_sel.close();}catch (SQLException e) { e.printStackTrace(); }

}

}>

Observar que podemos obtener diferentes clases de metadatos:

1. En nuestra aplicación lo primero es obtener acceso a metadatos de la base de datos (DatabaseMetaData):

2.3. DatabaseMetaData dbmd = con.getMetaData();

o La primera utilidad de acceder a los metadatos de la base de datos es examinar las características generales, por ejemplo:

oo dbmd.getMaxStatements()

Page 45: Primeros Pasos Con JDBC

o La segunda puede ser el acceso a las tablas: oo ResultSet rs = dbmd.getTables( null, null,

null, new String[]{"TABLE"} );

4. El segundo tipo de metadato (ResultSetMetaData) se obtiene a partir de un conjunto de resultados estándar (ResultSet):

5.6. ResultSet rs_sel = senten.executeQuery("SELECT * FROM "

+ nom_tabla );7. ResultSetMetaData mdata_sel = rs_sel.getMetaData();

// Obtener Metadatos de consulta

Sirve para acceder a información sobre las columnas. En el siguiente ejemplo se accede a información sobre nombre y tamaño de la columna:

System.out.print("\t" + mdata_sel.getColumnLabel(num_col_sel));

System.out.print(" (Size=" + mdata_sel.getColumnDisplaySize(num_col_sel) +")");

¿Cómo obtenemos todos los campos de una tabla? En la función mostrar_cols_tabla() la sentencia SQL "SELECT * ..." nos permite acceder a todos los campos de la tabla.

Volver al índice

Un applet Swing que utiliza JDBC

¿Qué hace el applet?

A continuación puedes ver un applet Swing que:

Usa un JTabbedPane (un panel de pestañas, de fácil manejo)

En el primer panel del JTabbedPane nos pide los datos para conectarse a una base de datos, especificando la tabla. Este applet le permite ver casi cualquier tabla de la base de datos.

En la pestaña "Constulta" puede ver el resultado de la sentencia SELECT, utilizando un JTable.

Page 46: Primeros Pasos Con JDBC

El diseño software sigue el patrón modelo-vista.

Las primeras opciones de driver y host hacen referencia a recursos de un servidor local. Las segundas opciones se refieren a un servidor remoto. En su caso se encuentra ante un applet remoto, por tanto debe escoger el driver del servidor: org.gjt.mm.mysql.Driver; además debería escoger el host remoto, es decir, jdbc:mysql://proactiva-calidad.com:3306. Con ello hacemos referencia al servidor de la base de datos (no al servidor web), indicando el puerto (3306) asignado al gestor (mysql) de base de datos. Escriba como password la palabra 'nebrija' (sin comillas y en minúsculas).

Si cambia la tabla y pone 'cliente', visualizará los clientes de las operaciones de venta.

Si el applet es local no tiene problemas en acceder a la base de datos local. Por lo mismo, si el applet está en el servidor no tendrá problemas en acceder a la base de datos remota (del servidor). El problema está en las situaciones mixtas:

Applet local accediendo a base de datos remota. Applet de servidor accediendo a base de datos local.

¿Por que estas situaciones son problematicas a priori? El applet esta limitado por severas restricciones de seguridad. No puede acceder a resursos ajenos a su espacio de trabajo. Vea el capitulo dedicado a la seguridad en la sección de applets, donde se expone una solución a estos problemas.

Diseño del applet: patrón modelo-vista

Se aplica el patrón modelo-vista:

Modelo: responsable del tratamiento de datos. Lo implentamos en modelo.java. Vista: responsable del interfaz y gestión de eventos. En nuestro ejemplo lo

implementamos en vista.class, donde se encuentra el JApplet.

Código fuente:

modelo.java vista.java

El modelo

Para más información sobre las tablas y sus modelos de datos: Tutorial de SUN sobre JTable

Vea que la clase modelo hereda de AbstractTableModel, más abajo explicaremos las razones. Lo que importa para empezar es ver que los atributos son en su mayoría clases JDBC que se instancian durante la conexión a la base de datos y la carga de datos:

class modelo extends AbstractTableModel { private Statement stat; private ResultSet rs; // Conjunto de datos de sentencia SELECT private ResultSetMetaData rsmd; // Necesario para nombres de columnas

Page 47: Primeros Pasos Con JDBC

private Connection con; // Conexión con base de datos private String mensaje_error = null;// Registro de mensajes de error private String sentencia; // Ultima sentencia ejecutada

/********************************************************************* Conecta a la base de datos y carga los datos. Devuelve true si ha ido bien. **********************************************************************/ public boolean conexion( String driver, String host_basedatos, String tabla, String login,

String pwd ) {

//// Si no puedo cargar driver de base de datos, muestro error y salgo

if ( !cargarDriver( driver ) ){ System.out.println( obt_mensaje_error() ); return false;}

//// Si no puedo conectar, muestro error y salgoif ( !conectar( host_basedatos, login, pwd ) ) { System.out.println( obt_mensaje_error() ); return false;}

//// Si puedo conectar, ...else { //// Si no puedo cargar datos, muestro error y salgo if ( !cargarDatos( tabla ) ) {

System.out.println( obt_mensaje_error() );return false;

}}return true;

}

/************************************************************************* * Carga el driver. Devuelve: * - true: todo ha ido bien * - false: hay un error. El mensaje de error se registra en atributo mensaje_error *************************************************************************/ private boolean cargarDriver( String driver) {

try { Class.forName( driver ); return true;}catch( ClassNotFoundException e ) { mensaje_error = new String( "No encuentra el driver. " +

e.getMessage() ); return false;}

}

Page 48: Primeros Pasos Con JDBC

....

Lo primero que debe hacer la clase modelo es cargar el driver del gestor de base de datos y a continuación conectarse a la base de datos para poder realizar la consulta. Esto último implica la ejecución de la sentencia SELECT. Estas funciones no tienen nada especial, ya hemos visto en otros capítulos como realizar la conexión y ejecución. Un ejemplo de código para la conexión:

private boolean conectar( String base, String login, String pwd ) {

try { con = DriverManager.getConnection( base, login, pwd ); return true;}catch (SQLException e) { mensaje_error = new String( "Error de conexión. " +

e.getMessage() ); return false;}

}

A la hora de la consulta el trabajo es sencillo, aunque en nuestro ejemplo hay algunas particularidades:

Manejamos un cursor (Resultset) que debe admitir desplazamiento. Esto depende del gestor de base de datos. En nuestro caso hemos solicitado un cursor o ResulSet que admita desplazamiento (aunque sea insensible a cambios de otros usuarios): TYPE_SCROLL_INSENSITIVE. Además solicitamos un cursor que nos permita cambiar los datos: CONCUR_UPDATABLE. En nuestro caso no modificamos los datos, pero se muestra a efectos pedagógicos. No todos los gestores de bases de datos tienen esta conjunción de características. Por ejemplo, Access no contempla esta capacidad. Volveremos más adelante a hablar de los cursores:

Manejamos ResultSetMetaData para poder determinar después el número de columnas y sus nombres.

El método actualizar() puede ver que ejecuta la sentencia SELECT, donde el nombre de la tabla esta parametrizado (puede leer casi cualquier tabla de la base de datos).

La llamada fireTableStructureChanged() es importante, le indica a la JTable que se debe actualizar, ya que los datos se han cargado (y puede haber cambiado la estructura de la JTable).

/********************************************** Ejecuta la sentencia SELECT, con lo que carga el ResultSet y el ResultSetMetaData. Devuelve: - true: todo ha ido bien

Page 49: Primeros Pasos Con JDBC

- false: hay un error. El mensaje de error se registra en atributo mensaje_error ************************************************/ private boolean cargarDatos( String tabla ) {

try { stat =

con.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_UPDATABLE );

boolean resultado = actualizar( tabla ); if ( resultado )

rsmd = rs.getMetaData(); // Obtenemos un ResultSetMetaData

return resultado;}catch (SQLException e) { mensaje_error = new String( e.getMessage() ); return false;}

}

/**************** actualizar() ************************/ public boolean actualizar( String tabla ) {

try { sentencia = "SELECT * FROM " + tabla; if (rs != null)

rs.close(); if ( stat != null ) {

rs = stat.executeQuery(sentencia); // Ejecutar la consulta

fireTableStructureChanged(); // Ordena a la JTable que se actualice

return true; } mensaje_error = new String( "No se pueden cargar los datos.

Probablemente no hay conexión" ); return false;}catch (SQLException e) { mensaje_error = new String( "No se pueden cargar los datos.

" + e.getMessage() ); return false;}

}

Observe que al crear el objeto de la clase Statement con createStatement() le indicamos a JDBC que queremos un ResultSet que al menos admita desplazamiento, aunque sea insensible a cambios de otros usuarios. Los ResulSet pueden tener o no desplazamento y pueden ser sensibles o no a los cambios en la base de datos que pueden producir otros usuarios o procesos. Veamos los tipos:

TYPE_FORWARD_ONLYSólo se puede recorrer hacia adelante y no es sensible a cambios en base de datos.

TYPE_SCROLL_INSENSITIVETiene desplazamiento (adelante/atrás), pero es insensible a cambios en base de datos.

TYPE_SCROLL_SENSITIVETiene desplazamiento (adelante/atrás) y es sensible a cambios en base de datos.

Page 50: Primeros Pasos Con JDBC

Tipos de ResulSet en función de si pueden hacer cambios en la base de datos:

CONCUR_READ_ONLY El ResultSet no puede modificar la base de datos.

CONCUR_UPDATABLE El ResultSet puede modificar la base de datos.

No lo incluimos en nuestro código, pero puede comprobar que el ResultSet tiene las capacidades deseadas. Con la siguiente función comprobamos que el ResultSet tenga desplazamiento:

/********************************************************** Si la base de datos admite scroll, devuelve true Si no: registra el error en atributo mensaje_error y devuelve false ***********************************************************/ private boolean admite_scroll() throws SQLException {

/* Obtener metadatos de base de datos */DatabaseMetaData dbmd = con.getMetaData();

/***** Si la base de datos no admite ResultSet con Scroll, lo indicamos y salimos ****/

if ( dbmd.supportsResultSetType( ResultSet.TYPE_FORWARD_ONLY ) ) {

mensaje_error = new String( "Esta base de datos no admite cursor con desplazamiento (scroll)");

return false;}

return true; }

Una vez que hemos solventado el problema de la conexión a base de datos y de la ejecución de la sentencia SELECT, veamos cómo programar la tabla (JTable).Varios controles Swing (entre ellos JTable) están diseñados para que el programador diferencie dos componentes:

La vista (por ejemplo la clase JTable), que es responsable de los aspectos de visualización.

El modelo de datos del JTable (por ejemplo la clase AbstractTableModel), responsable del tratamiento de datos.

Se puede trabajar con una JTable sin un modelo, al principio parece que incluso puede ahorrarse líneas de código, pero es una falsa impresión, ya que a a la larga la reusabilidad, eficiencia y legibilidad de los JTable mejora si usa un modelo abstracto. Los modelos de datos de JTable heredan de AbstractTableModel. El constructor de la tabla (JTable) nos permite pasar como argumento una clase AbstractTableModel:

public JTable(AbstractTableModel dm)

Por ello nuestra clase modelo hereda de AbstractTableModel:

class modelo extends AbstractTableModel { ....

Como nuestra clase hereda de la clase abstracta AbstractTableModel debemos redefinir al menos una seríe de métodos:

Page 51: Primeros Pasos Con JDBC

public int getColumnCount( ): el modelo informa a su JTable del número de columnas.

public int getRowCount(): idem para el número de filas public Object getValueAt( int fila, int col ): el modelo informa a su JTable de lo que

tiene que escribir en la celda señalada por (fila,columna), esta información es el Object que devuelve.

Programando con JDBC tenemos una ventaja que nos ahorra bastante trabajo, el ResultSet y su ResultSetMetaData nos ofrecen funcionalidades para navegar y modificar los datos. Vea que se usa el objeto rsmd (del tipo ResultSetMetaData para determinar el número y título de las columnas:

public String getColumnName( int c ) { try {

if ( rsmd != null ) return rsmd.getColumnName(c + 1); return "";

} catch(SQLException e) { e.printStackTrace(); return ""; } }

/*************** getColumnCount() ******************/ public int getColumnCount() { try {

if ( rsmd != null ) return rsmd.getColumnCount(); return 0;

} catch(SQLException e) { e.printStackTrace(); return 0; } }

/****************** getRowCount() *******************/ public int getRowCount() {

try { if ( rs != null ) {

rs.last(); // Nos situamos en la última filareturn rs.getRow(); // Devolvemos el número de la fila

} return 0;}catch(SQLException e) { e.printStackTrace(); return 0; }

}

/******************* getValueAt() *********************/ public Object getValueAt( int fila, int col ) {

try { if ( rs != null ) {

rs.absolute( fila + 1 );return rs.getObject( col + 1 );

} return "";}catch(SQLException e) { e.printStackTrace(); return null; }

}

Observe que hay una pequeña asimetría: las filas y columnas de un JTable empiezan a contarse desde cero. Mientras que los ResultSet cuentan las filas desde uno. Por ello, por ejemplo, la fila tres de la tabla es la fila cuatro del ResultSet. Ver a este respecto la

Page 52: Primeros Pasos Con JDBC

implementación de getValueAt(): la posición 'fila' de la JTable equivale a la posición 'fila+1' del ResultSet.

No lo hemos puesto en este ejemplo, pero gracias a los ResultSet podemos modificar de forma sencilla los datos. Un ejemplo de código para borrar un registro podría ser:

boolean borrar( int fila ) {try { rs.absolute( fila + 1 ); rs.deleteRow(); return true;}catch (SQLException e) { mensaje_error = new

String( e.getMessage() ); return false; } }

Si no lo hubieramos hecho por medio del cursor del ResultSet, tendriamos que haber recurrido a una sentencia "DELETE ..." como agumento de una llamada a ResultSet.executeUpdate().

La vista

La vista no tiene nada especial:

1. Tiene como atributo el modelo:

public class vista extends JApplet { JTable tabla;

modelo mod; // modelo de datos

...

2. Lo instancia en el método init():

//// Crea un modelo vacio (no hay conexión a la base de datos todavía) mod = new modelo();

3. Se lo asigna a la JTable:

tabla = new JTable( mod);

4. Además debemos asignar la JTable a un panel con scroll:

JScrollPane pnlScroll = new JScrollPane( tabla );

En nuestro código usamos un JTabbedPane, un panel con "pestañas". Le asignamos los subpaneles de forma sencilla:

Page 53: Primeros Pasos Con JDBC

pnlTab.addTab( "Conexión", pnlFormulario ); pnlTab.addTab( "Consulta", pnlConsulta );

En nuestro caso queremos además que si la conexión ha tenido éxito, se cambie de forma automática al panel de consulta:

pnlTab.setSelectedComponent( pnlConsulta );

En cuanto a la gestión de eventos de botón el planteamiento es sencillo. Vamos a recibir los eventos en nuestra clase del tipo JApplet, por tanto hacemos que esta clase implemente el interfaz adecuado:

public class vista extends JApplet implements java.awt.event.ActionListener {

Por el sólo hecho de utilizar este interfaz debemos implementar el método actionPerformed(). Para que este método sea llamado como consecuencia de los eventos debemos indicar que el listener u oyente del evento será nuestro applet (this):

btnConexion.addActionListener( this );

Volver al índice

ResultSet con actualización

Introducción

En capítulos anteriores hemos podido obtener un conjunto de resultados a partir de una consulta. Además hemos visto como nos podemos desplazar por el conjunto de resultados. A continuación veremos como se pueden modificar los datos del conjunto y volcar las modificaciones en la base de datos.

Lo primero es comprobar si nuestro gestor de base de datos nos permite modificar conjuntos de resultados. Existen unas constantes static en la clase ResultSet que identifican el ResulSet en función de si pueden o no actualizar la base de datos:

CONCUR_READ_ONLY El ResultSet no puede modificar la base de datos.

CONCUR_UPDATABLE El ResultSet puede modificar la base de datos.

Una vez que hayamos establacido la conexión, tenemos que obtener un objeto de la clase Statement que admita actualizaciones de ResultSet. Para ello tenemos una versión de createStatement que dispará una excepción del tipo SQLException, en el caso de que la base de datos no permita el tipo señalado en resultSetType o resultSetConcurrency:

Page 54: Primeros Pasos Con JDBC

public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException

Como ejemplo:

String orden_SQL = "SELECT codigo, nombre FROM cliente ORDER BY nombre";

Statement sentencia = con.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);

ResultSet rs = sentencia.executeQuery( orden_SQL );

Lo anterior es una forma sencilla y adecuada de tener un ResultSet actualizable. Veamos a continuación una forma erronea de crear un ResultSet actualizable. Si el objeto de tipo Statement se ha obtenido con la llamada a createStatement sin argumentos:

Statement sentencia = con.createStatement( );Nos puede ocurrir que al modificar el ResultSet: /*** Nos ponemos en el primero y lo modificamos ***/

rs.first(); rs.updateString(

"nombre", "Joaquín"); rs.updateRow();

Se dispare una excepción. ¿Por qué? Estamos modificando un conjunto de resultados, pero el Statement del que proviene no admite esta capacidad: com.mysql.jdbc.NotUpdatable: Result Set not updatable.This result set must come from a statement that was created

with a result set type of ResultSet.CONCUR_UPDATABLE

También podemos saber si el conjunto permite actualización mediante un método de ResulSet:

public int getConcurrency()

Que devuelve la capacidad del ResultSet en la forma de las constantes antes descritas. Otro método para averiguar la capacidad de un ResultSet implica el manejo de metadatos, que ya hemos visto en otros capítulos:

boolean DatabaseMetaData.supportsResultSetConcurrency(int type, int concurrency) throws SQLException

Dispará una excepción del tipo SQLException, en el caso de que la base de datos no permita el tipo señalado en resultSetType o resultSetConcurrency.

Hay que tener en cuenta que no todas las consultas que nos devuelven un conjunto de resultados nos permiten actualización (aunque el gestor de base de datos si lo permita). La razón de esto puede ser que la consulta implique a varias tablas y que no estén enlazadas por el enlace de clave primaria - clave externa o que incluyendo varias tablas la consulta no incluya las claves primarias.

Actualizar

Unas sencillas líneas de ejemplo:

/***** Definir sentencia y ejecutarla ********/

Page 55: Primeros Pasos Con JDBC

String orden_SQL = "SELECT codigo, nombre FROM cliente ORDER BY nombre";

Statement sentencia = con.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);

ResultSet rs = sentencia.executeQuery( orden_SQL );

/*** Nos ponemos en el primero y lo modificamos ***/

rs.first(); rs.updateString( "nombre", "Joaquín");

rs.updateRow();

Lo primero es obtener un objeto Statement por medio de crateStatement; este objeto permite scroll (desplazamiento) y actualización . A continuación nos situamos mediante first() en la fila que queremos modificar (la primera) y realizamos el cambio de la columna "nombre" con updateString(), indicando en su segundo argumento el nuevo valor ("Joaquín"). Hay una versión de updateXXX() para cada tipo de campo: String, Double, etc.

Es importante destacar que la modificación se refiere al ResultSet, concretamente a la fila actual del ResultSet, y si nos movemos a otra fila los cambios se perderán, a menos que antes del desplazamiento llames a updateRow(). Unicamente updateRow() vuelca los cambios a la base de datos. Con el método cancelRowUpdates() cancelamos las modificaciones DE LA FILA ACTUAL.

Inserción y borrado

Después de haber aprendido la actualización, la inserción no resulta difícil. Lo primero es desplazar el cursor a una posición especial, una pseudofila en blanco, mediante una llamada a moveToInsertRow() de la clase ResultSet. A continuación usamos updateXXX( String nombre_columna, nuevo_valor ) para dar valores a las columnas:

rs.moveToInsertRow(); rs.updateString( "codigo", "OJD33"); rs.updateString(

"nombre", "Pedro Juan"); rs.updateInt( "edad", 34 ); rs.insertRow();

// Guardar en base de datos rs.moveToCurrentRow(); // Volvemos

a la posición anterior a hacer moveToInsertRow

Con moveToCurrentRow() movemos el cursor a la posición anterior a la llamada a moveToInsertRow. En nuestro ejemplo la posición que ocupa el registro insertado depende de la cláusula ORDER BY de la consulta.

El borrado es extremadamente sencillo: el método deleteRow() borra la fila activa tanto del ResultSet como de la base de datos.

Sensatez

El manejo de ResultSet con actualización es una herramienta sencilla y muy utilizada. Pero conviene hacer alguna consideración:

1. Resulta conveniente en aquellas aplicaciones en donde el usuario puede modificar de forma inmediata y extensa los registros. Por ejemplo, si presentas una tabla (JTable) y

Page 56: Primeros Pasos Con JDBC

das al usuario la libertad para modificar cualquier fila. En cualquier otro caso es más eficiente usar las sentencias SQL INSERT, UPDATE o DELETE.

2. Tener en cuenta las desventajas comparativas respecto a usar sentencias SQL con executeUpdate(): con un ResultSet hay que ejecutar una sentencia SELECT, localizar la fila adecuada y realizar la actualización. Esto puede ser menos eficiente que ejecutar directamente una sentencia SQL.

Volver al índice

Uso de JNDI para utilizar un pool de conexiones

Ramiro Lago (Noviembre 2005, Mayo 2007)

Concepto de pool de conexiones

Una aproximación elemental a JDBC implica que se realiza una conexión a la base de datos en cada servlet. Se repite el esquema conexión-operación-desconexión. Esta forma de trabajar es perfectamente válida, pero resulta ineficiente, ya que se están desperdiciando ciclos de ejecución en cada conexión y desconexión.

En vez de esta orientación hay otra alternativa: crear un pool de conexiones, es decir, mantener un conjunto de conexiones. Cuando un servlet/JSP requiere una conexión la solicita al conjunto de conexiones disponibles (desocupadas, "idle") y cuando no la va a usar la devuelve al pool. De esta forma nos ahorramos el consumo de tiempo de conexión y desconexión. La mayor parte de servidores de aplicaciones (Tomcat, WebSphere, etc.) incluyen clases que implementan un pool de conexiones. Nosotros vamos a poner un ejemplo con MySQL 5.0 y Tomcat 5.5, concretamente con DataBase Common Pooling (DBCP) de Tomcat. El pool debe gestionar situaciones anómalas, como el cierre involuntario de una conexión después de cerrar un ResultSet o un Statement.

A continuación le mostraremos cómo implementar el servlet, pero esto no es problemático. Lo que puede llegar a ser una fuente de dolores de cabeza es la correcta configuración de server.xml teniendo en cuenta que dicha configuración es altamente dependiente de las versiones de base de datos y sobre todo de servidor de aplicaciones.

Definir un DataSource

Ya hemos visto anteriormente (ver) que gracias a JNDI podemos acceder a objetos definidos en web.xml. Ahora vamos a mostrar un servlet que usa JNDI para acceder a un pool de conexiones. El tipo de dato que utilizamos en web.xml es un DataSource, una representación genérica de una fuente de datos.

&ltenv-entry> &ltenv-entry-name>ejemplos/conexion</env-entry-name> &ltenv-entry-type>javax.sql.DataSource</env-entry-type> &ltenv-entry-auth>Container</env-entry-auth> </env-entry>

Debemos tener en CATALINA_HOME/commons/lib (o el CLASSPATH) el driver JDBC: por ejemplo, mysql-connector-java-3.1.6-bin.jar (MySQL)

Page 57: Primeros Pasos Con JDBC

El método init()

Con el servlet lo primero que hacemos es crear una fuente de datos:

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

public class server_xml01 extends HttpServlet {

private DataSource fuenteDatos = null; String errorInicial = null;

/******************************************************************************** * INIT: creo una fuente de datos (DataSource) ********************************************************************************/ public void init(ServletConfig config) throws ServletException { try {

Context contextoInicial = new InitialContext(); // Equivalente: new InitialContext(null).

Context contexto = (Context) contextoInicial.lookup("java:comp/env");

fuenteDatos = (DataSource) contexto.lookup( "ejemplos/conexion"); } catch(NameNotFoundException e) { // Hija de NamingException

errorInicial = new String(e.toString()); } catch(Exception e) {

errorInicial = new String(e.toString()); } }

En doGet()

El servlet en doGet() solicita una conexión a la fuente de datos por medio de getConnection(). Observar que el objeto Connection es  local al método, ya que lo declaramos para recibir una conexión del pool y luego lo devolvemos al pool. Decimos "devolver" ya que con el uso del pool el programador no cierra conexiones, sino que las devuelve al pool.

public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); Connection con = null; String errorPeticion = null;

try {

Page 58: Primeros Pasos Con JDBC

//// Si no hay error en init(): solicito conexión al pool if ( errorInicial == null ) {

con = fuenteDatos.getConnection(); }

} catch( Exception e) {

errorPeticion = new String(e.toString()); } finally {

//// Escribo página con resultado de la búsqueda out.println("&lthtml>"); out.println("&lthead>&lttitle>Ejemplo de Servlet y

JNDI</title></head>"); out.println("&ltbody bgcolor=\"#FFFF9D\">&ltfont

color=\"#000080\" FACE=\"Arial,Helvetica,Times\" SIZE=2>"); out.println("&ltCENTER>&ltH3>Usa JNDI para recuperar

propiedades de web.xml</H3></CENTER>&ltHR>"); out.println("&ltP>init:" + ((errorInicial==null) ? "No hay

error" : errorInicial) + "</P>"); out.println("&ltP>request:" + ((errorPeticion==null) ? "No hay

error" : errorPeticion) + "</P>"); out.println("&ltp>Conexión: " + ((con==null) ? "No" :

con.toString()) + "</p>");

//// Realizo consulta out.println("&ltp>Select:</p>&ltul>"); Statement sentencia = null; ResultSet rs = null; try { sentencia = con.createStatement(); rs = sentencia.executeQuery("select * from cliente");

while ( rs.next() ) { out.println( "&ltli>" + rs.getString(1) + ", " +

rs.getString(2) + "</li>"); } rs.close(); sentencia.close(); con.close(); // Se devuelve la conexión al pool } catch (Exception e) { out.println( "&ltli>Error en la consulta</li>");

//// Asegurar que result sets and statements son cerrados, //// y que la conexión retorna al pool if (rs != null) { try { rs.close(); } catch (SQLException sqle) {} } if (sentencia != null) { try { sentencia.close(); } catch (SQLException sqle) {} } if (con != null) {

try { con.close(); } catch (SQLException sqle) {} } }

out.println("</ul></font></body></html>"); } }

La consulta no tiene nada de particular. Se crea el objeto Statement a partir de la conexión obtenida del pool. Observar que cerramos (close) el ResulSet, Statement y Connection

Page 59: Primeros Pasos Con JDBC

(aunque en realidad ya hemos dicho que no se destruye la conexión, sino que se devuelve al pool). Una forma segura de devolver la conexión al pool:

public static void cerrarConexion( Connection con ) {try { if ( con != null ) if ( !con.isClosed() ) // Si no está

cerrada, la cierro con.close();}catch (SQLException e) { e.printStackTrace(); }

}

Acceso al servlet.

Documentación de Tomcat 5.5: JNDI Datasource HOW-TO

Documentación de Tomcat 5.5: JNDI Resources HOW-TO

Configurar server.xml

A continuación los parámetros de configuración de server.xml. Es importante observar que el orden puede ser relevante en función de la versión de servidor de aplicacicones. Observar que name de ResorceParams debe ser el mismo que el de Resource y a su vez debe coincidir con env-entry-name de web.xml. Las expresiones que ve a continuación se deben de colocar dentro de tu contexto (etiqueta Context) del archivo server.xml

&ltContext>....

&ltResource

name="ejemplos/conexion" auth="Container"

type="javax.sql.DataSource"maxActive="100"maxIdle="30"maxWait="10000"

username="usuario"password="pwd"

driverClassName="com.mysql.jdbc.Driver"

url="jdbc:mysql://localhost:3306/base_datos?autoReconnect=true" />

</Context>

Explicación de algunos parámetros:

Use the removeAbandonedTimeout parameter to set the number of seconds a dB connection has been idle (libre o desocupada) before it is considered abandoned. Por defecto: 300

Page 60: Primeros Pasos Con JDBC

maxActive: Maximum number of dB connections in pool. Make sure you configure your mysqld max_connections large enough to handle all of your db connections. Set to 0 for no limit.

maxIdle: Maximum number of idle dB connections to retain in pool. Set to 0 for no limit. maxWait: Maximum time to wait for a dB connection to become available in ms, in this

example 10 seconds. An Exception is thrown if this timeout is exceeded. Set to -1 to wait indefinitely.

url: The JDBC connection url for connecting to your MySQL dB. The autoReconnect=true argument to the url makes sure that the mm.mysql JDBC Driver will automatically reconnect if mysqld closed the connection. mysqld by default closes idle connections after 8 hours.

Volver al índice