Anda di halaman 1dari 55

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.
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:

○ 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");

○ 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:


○ Controlador: controlador de base de datos, en nuestro ejemplo es mysql.
○ Host: servidor de base de datos (por defecto es localhost).
○ Failoverhost: servidor de respaldo, en caso de que falle el primero
○ Port: si no se indica puerto, se toma el valor por defecto. En el caso de MySQL
es 3306.
○ 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.
○ 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:
○ En MySQL local: Connection con =
DriverManager.getConnection( "jdbc:mysql://localhost/prueba", "log", "pwd" );
○ 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;
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ón


Si no se hubiera producido, se habría disparado SQLException
Un ejemplo con registro estático
En el segundo ejemplo podemos observar:
• 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 props
in.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 ***/
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(); }
}
}
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:
Principio del formulario

proactiv_prueba

Base:
proactiv_alumnos

Usuario:
*******

Password:

Entrar

Final del formulario


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:
○ base: es el nombre de la base de datos. Tipo: text.
○ login: es el login del usuario. Tipo: text.
○ password: es el password del usuario. Tipo: PASSWORD
Manejo de un archivo .properties
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.Driver
basedatos.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.Driver
basedatos.host=jdbc:mysql://proactiva-calidad.com:3306/
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'.
* 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".
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 salida
4. 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ágina
9. 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. }

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. else
30. 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:


• 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().

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
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:
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
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 );
}
}

/*****************************************************************
***
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
******************************************************************
*/
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(); }
}

/*****************************************************************
**
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, 33
55B, Fernando, 28
105A, José, 21
Listado de los clientes entre 21 y 30 años:
55B, Fernando, 28
105A, José, 21
Listado de los clientes entre 21 y 30 años con sus ventas
correspondientes:
55B, Fernando, 28, 4090.0, 3450.0
105A, José, 21, 3500.0, 3180.0
105A, José, 21, 3600.0, 2980.0
Listado de ingresos, costes y beneficio de clientes:
Ana, 230A, 5070.0, 5275.0, -205.0
Fernando, 55B, 4090.0, 3450.0, 640.0
José, 105A, 7100.0, 6160.0, 940.0
Algunas consideraciones sobre el ejemplo:
• 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.
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( "SE
LECT 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
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
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>
&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>
....

Principio del formulario


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

proactiv_alumnos

Usuario:
*******

Password:

Entrar

Final del formulario

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.
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 {
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 request
sesion.setAttribute("basedatos",
dc.getPropiedades().getHostBaseDatos()+

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ágina
UtilGeneral.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.

///// Inicio de tabla HTML


out.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 HTML


out.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 tabla


UtilGeneral.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:

&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>");
}
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;
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

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) {
UtilGeneral.imprimir(out, "Error en la
consulta. " + e.getMessage());
}

//// Inicio de tabla


out.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úmeros


DecimalFormat 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 tabla


for ( 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

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;
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 );

//// 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();
• }
• 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.*;

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
*************************************************************/
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(); }
}

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 Metadatos
int 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 fila
for ( int i = 1; i <= num_columnas; i++)
{ // Recorro las columnas
if ( 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
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: MySQL
Versión de BD: 4.0.18-max-debug
Nombre de controlador: MySQL-AB JDBC Driver
Versió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: 3
Versión menor de controlador JDBC: 0
Statements simultaneos: 0
Soporte SQL92: false
Soporte de SCROLL_SENSITIVE: false
Ademá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:

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: " +

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();
}
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();

○ La primera utilidad de acceder a los metadatos de la base de datos es


examinar las características generales, por ejemplo:

○ dbmd.getMaxStatements()

○ La segunda puede ser el acceso a las tablas:



○ 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.

• 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
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 salgo


if ( !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;
}
}

....
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
- 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.CONCU
R_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:
Sólo se puede recorrer hacia adelante y no es sensible a cambios en base
TYPE_FORWARD_ONLY
de datos.
Tiene desplazamiento (adelante/atrás), pero es insensible a cambios en
TYPE_SCROLL_INSENSITIVE
base de datos.
TYPE_SCROLL_SENSITIVE Tiene desplazamiento (adelante/atrás) y es sensible a cambios en base de
datos.
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:
• 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 fila
return 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
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:

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:
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 ********/
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
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)
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
{
//// 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
(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
• 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

Anda mungkin juga menyukai