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:


    ....
  <servlet>
    <servlet-name>FormClientes</servlet-name>
    <servlet-class>docen_servlet01.JDBC01.presentacion.FormClientes</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>FormVentas</servlet-name>
    <servlet-class>docen_servlet01.JDBC01.presentacion.FormVentas</servlet-class>
  </servlet>    
  <servlet-mapping>
    <servlet-name>FormClientes</servlet-name>
    <url-pattern>/servlet/FormClientes</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>FormVentas</servlet-name>
    <url-pattern>/servlet/FormVentas</url-pattern>
  </servlet-mapping>
  ....
    
Formulario de ejemplo que muestra los clientes y sus ventas, anidando servlets:

Usuario:  

Password:















Pede obtener el código del ejemplo.

El ejemplo: los clientes y sus pedidos

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

  1. El primer servlet realiza un formulario con una lista de clientes.
  2. El segundo servlet recibe el cliente seleccionado y consulta en la base de datos las ventas de dicho cliente. El resultado es una página con las ventas del cliente.

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

Applet


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("<table BORDER=1 align=center cellpadding='10' cellspacing=1>");
			out.println("<tr><td bgcolor=#00FF00>");
			out.println("<FONT color=#000080 FACE='Arial,Helvetica,Times' SIZE=2>");
			
			///// Inicio del formulario HTML
			out.println("<form 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("<SELECT NAME='cliente' onchange=\"copiarValor('nombre','codigo');\" id='nombre'>");
			for ( int i = 0; i< vecClientes.size(); i++ ) {
				Cliente c = (Cliente) vecClientes.get(i);
				out.println("<OPTION 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("<SELECT 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("<OPTION 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("<INPUT TYPE=HIDDEN NAME='cliente.codigo' id='codigo'>");
			
			//// Poner botón y fin de formulario y de tabla
			UtilGeneral.imprimir( out, "<input 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:


<script 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("<INPUT 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:

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("<P><B>Sesion:</B>" + sesion.getId() + ". Atributos:" + "<OL>");
			for (Enumeration e = sesion.getAttributeNames(); e.hasMoreElements(); ) {
				String atrib = (String) e.nextElement();
				out.print("<LI>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("<TD align=right>" + myFormatter.format( precio ) + "</TD>");
	out.println("<TD align=right>" + myFormatter.format( coste ) + "</TD>");
	out.println("<TD 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("<P>Ventas al cliente " + request.getParameter( "cliente" ) + ":");
			out.println("<TABLE BORDER=1 align='center'>");
			out.println("<TR  bgcolor=#00CCFF>");
			out.println("<TH>CODIGO</TH>");
			out.println("<TH>PRECIO</TH>");
			out.println("<TH>COSTE</TH>");
			out.println("<TH>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("<TR bgcolor=#00FF00>");
				out.println("<TD>" + v.getCodigo() + "</TD>");
				float precio = v.getPrecio().floatValue();
				float coste = v.getCoste().floatValue();
				out.println("<TD align=right>" + myFormatter.format( precio ) + "</TD>");
				out.println("<TD align=right>" + myFormatter.format( coste ) + "</TD>");
				out.println("<TD 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