<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <%@ include file="/WEB-INF/jsp/include.jsp" %> Internacionalización con Spring

Persistencia en Spring (II)

Ramiro Lago Bagüés (Enero 2008)


Introducción al ejemplo

El manejo de persistencia en Spring es un recubrimiento sobre JDBC para dotarlo de un mayor nivel de abstracción. Lo explicaremos por medio de un ejemplo, una sencilla página que realiza operaciones sobre los clientes de una empresa. La estructura del ejemplo aparece a continuación, destacando cuatro grandes bloques: vista (html y jsp), dominio, persistencia, etiquetas y recursos.

ADVERTENCIA: Las JSPs se han colocado fuera de WEB-INF. Más adelante, aprenderemos con Spring MVC a situarlas en WEB-INF/jsp, que es un lugar más protegido.

Funciones sobre la tabla cliente:

Lo que se requiere conocer:

Arquitectura:

Nota sobre los jar:

A continuación una muestra de la página:

Este ejemplo muestra una página muy densa (diversos SELECT, formulario, etc.), por razones pedagógicas. Un caso real trataría de separar las consultas más costosas (SELECTS) del formulario.


Contexto de aplicación y pool de conexiones

La creación de la factoría y la carga de propiedades se realiza en una clase, con la finalidad de recubrir la implementación. El código tiene una particularidad: el archivo de properties no se especifica en el application-context.xml (que es lo más habitual), sino que se especifica en el el código Java, por medio de la clase PropertyPlaceholderConfigurer (por razones puramente pedagógicas, para aprender otra forma de cargar properties):


package com.recursos;

import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import com.persistencia.*;

/************************************************************************************
 * Carga del archivo de contexto y del archivo de properties
 *************************************************************************************/
public class GestorCarga {
	private XmlBeanFactory factory;
	private PropertyPlaceholderConfigurer cfg;
	private DAOClienteSpring dao;

	/*******************************************************************************
	 * Inyecta las properties (cfg) en la factoria, cuando se han cargado las
	 * definiciones de bean, pero todavía no se han instanciado
	 *******************************************************************************/
	public GestorCarga() throws Exception {
		factory = new XmlBeanFactory(new ClassPathResource("../application-context.xml"));
		cfg = new PropertyPlaceholderConfigurer();
		cfg.setLocation(new ClassPathResource("../configuracion.properties"));
		cfg.postProcessBeanFactory(factory);
	   	dao = (DAOClienteSpring) factory.getBean( "DAOCliente");
	}

	public DAOClienteSpring getDao() {
		return dao;
	}

	public PropertyPlaceholderConfigurer getCfg() {
		return cfg;
	}

	public void setCfg(PropertyPlaceholderConfigurer cfg) {
		this.cfg = cfg;
	}

	public XmlBeanFactory getFactory() {
		return factory;
	}

	public void setFactory(XmlBeanFactory factory) {
		this.factory = factory;
	}
}

Otra particularidad es que además de crear la factoría e inyectarle las properties, crea el DAO del cliente en el constructor. Puesto que es un ejemplo con un único DAO esto puede servir. Pero si hubiese numerosos DAOS se podría probar con otra estrategia. Por ejemplo, el correspondiente método getDaoXXX() devuelve el DAO y si no se ha cargado (si es null) entonces invoca a getBean().

El archivo index.jsp instancia GestorCarga como si fuera un Java Bean:


<jsp:useBean id="gestorCarga" scope="application" class="com.recursos.GestorCarga"/>

El archivo de definición del contexto de aplicación tiene únicamente dos bean: uno para la fuente de datos (Data Base Common Pooling, dbcp, de Apache) y otro para el DAO (observar que un atributo del DAO es la fuente de datos):


	<!-- Las propiedades del dataSource tienen como valor properties -->
	<bean id="dataSource" class="org.apache.tomcat.dbcp.dbcp.BasicDataSource" destroy-method="close">
		<property name="driverClassName" value="${jdbc.driverClassName}"/>
		<property name="url" value="${jdbc.url}"/>
		<property name="username" value="${jdbc.username}"/>
		<property name="password" value="${jdbc.password}"/>
	</bean>
	<!-- El DAO que tiene como atributo el dataSource -->
	<bean id="DAOCliente" class="com.persistencia.DAOClienteSpring">
		<property name="dataSource" ref="dataSource"/>
	</bean>

El dominio

Es algo sencillo, la clase que representa al cliente (el típico bean asociado a tabla):


package com.dominio;

public class Cliente {
	private String codigo;
	private String nombre;
	private String ape1;
	private String ape2;
	private Integer edad;

	public String getApe1() {
		return ape1;
	}
	public void setApe1(String ape1) {
		this.ape1 = ape1;
	}
	public String getApe2() {
		return ape2;
	}
	public void setApe2(String ape2) {
		this.ape2 = ape2;
	}
	public String getCodigo() {
		return codigo;
	}
	public void setCodigo(String codigo) {
		this.codigo = codigo;
	}
	public Integer getEdad() {
		return edad;
	}
	public void setEdad(int edad) {
		this.edad = new Integer(edad);
	}

	public void setEdad(Integer edad) {
		if (edad == null)
			this.edad = 0;
		else
			this.edad = edad;
	}

	public String getNombre() {
		return nombre;
	}
	public void setNombre(String nombre) {
		this.nombre = nombre;
	}
}

Persistencia

El DAO hereda de JdbcDaoSupport de Spring, que contiene el atributo "dataSource". En el application-context.xml se indica el bean de nuestra clase DAO y su atributo "dataSource".

Si se hecha un rápido vistazo al DAO se puede ver que se usa con frecuencia org.springframework.jdbc.core.simple.SimpleJdbcTemplate. Una de las clases más importantes de Spring 2.0 para encapsular la complejidad de usar JDBC. En un ejemplo típico creamos el SimpleJdbcTemplate (pasandole el DataSource de JdbcDaoSupport) y ejecutamos una SELECT por medio de query().


		SimpleJdbcTemplate template = new SimpleJdbcTemplate(getDataSource());
		return template.query("SELECT * FROM cliente", new ClienteMapper());

El segundo argumento de query() es un mapper, del que hablaremos más adelante. El código del DAO:


package com.persistencia;

import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import java.util.List;
import com.persistencia.mappers.ClienteMapper;
import com.persistencia.query.ClientePorEdadSetter;
import com.dominio.Cliente;

/*************************************************************************
 * DAO que emplea la clase JdbcDaoSupport de Spring
 *************************************************************************/
public class DAOClienteSpring extends JdbcDaoSupport {

	/*************************************************************************
	 * Inicializador de DAO
	 *************************************************************************/
	protected void initDao() throws Exception {
		super.initDao();
	}

	/*************************************************************************
	 * Count de clientes. Usa SimpleJdbcTemplate (Spring 2.0)
	 *************************************************************************/
	public int getNumeroClientes() {
		return new SimpleJdbcTemplate(getDataSource()).queryForInt("SELECT COUNT(*) FROM cliente");
	}

	/*************************************************************************
	 * Count de clientes entre un rango de edades. Usa SimpleJdbcTemplate (Spring 2.0)
	 *************************************************************************/
	public int getNumeroClientes(Integer edadMinima, Integer edadMaxima) {
		return new SimpleJdbcTemplate(getDataSource()).queryForInt(
				"SELECT count(*) FROM cliente WHERE edad >= ? AND edad <= ?",
				edadMinima, edadMaxima );
	}

	/*************************************************************************
	 * Select de todos los clientes. Usa un mapper. Usa SimpleJdbcTemplate (Spring 2.0)
	 *************************************************************************/
	public List<Cliente> selectAll() {
		SimpleJdbcTemplate template = new SimpleJdbcTemplate(getDataSource());
		return template.query("SELECT * FROM cliente", new ClienteMapper());
	}

	/*************************************************************************
	 * Select de los clientes entre un rango de edades. Usa mapper
	 * Usa PreparedStatement
	 *************************************************************************/
	public List<Cliente> selectAllPreparedStatement(int edadMinima, int edadMaxima) {
		return getJdbcTemplate().query( "SELECT * FROM cliente WHERE edad >= ? AND edad <= ?",
		    new ClientePorEdadSetter(edadMinima, edadMaxima),new ClienteMapper());
	}

	/*************************************************************************
	 * Select de UN cliente. Usa un mapper. Usa SimpleJdbcTemplate (Spring 2.0)
	 *************************************************************************/
	public Cliente selectCliente( String codigo ) {
		SimpleJdbcTemplate template = new SimpleJdbcTemplate(getDataSource());
		return (Cliente) template.queryForObject("SELECT * FROM cliente WHERE codigo=?",
				new ClienteMapper(), codigo);
	}


	/*************************************************************************
	 * Select para funciones mátemáticas básicas
	 *************************************************************************/
	public long getEdadTotal() {
		return getJdbcTemplate().queryForLong("SELECT SUM(edad) FROM cliente");
	}
	public long getEdadMedia() {
		return getJdbcTemplate().queryForLong("SELECT AVG(edad) FROM cliente");
	}
	public long getEdadMaxima() {
		return getJdbcTemplate().queryForLong("SELECT MAX(edad) FROM cliente");
	}
	public long getEdadMinima() {
		return getJdbcTemplate().queryForLong("SELECT MIN(edad) FROM cliente");
	}

	/*************************************************************************
	 * Insert de cliente con SimpleJdbcTemplate (Spring 2.0)
	 * Usa ClienteMapper para inyectar los datos en la sentencia SQL
	 *************************************************************************/
	public int insertarCliente(Cliente cliente) {
		SimpleJdbcTemplate template = new SimpleJdbcTemplate(getDataSource());
		return template.update("INSERT INTO cliente (codigo, nombre, ape1, ape2, edad) " +
				"VALUES (?, ?, ?, ?, ?)", ClienteMapper.mapAributos(cliente) );
	}
	/*************************************************************************
	 * Actualiación de cliente con SimpleJdbcTemplate (Spring 2.0)
	 * Usa ClienteMapper para inyectar los datos en la sentencia SQL
	 *************************************************************************/
	public int actualizarCliente(Cliente cliente) {
		SimpleJdbcTemplate template = new SimpleJdbcTemplate(getDataSource());
		return template.update("UPDATE cliente SET codigo=?, nombre=?, ape1=?, "+
				"ape2=?, edad=? WHERE codigo='" + cliente.getCodigo() + "'",
				ClienteMapper.mapAributos(cliente));
	}
	/*************************************************************************
	 * Borrado de cliente con SimpleJdbcTemplate (Spring 2.0)
	 *************************************************************************/
	public int borrarCliente(Cliente cliente) {
		SimpleJdbcTemplate template = new SimpleJdbcTemplate(getDataSource());
		return template.update("DELETE FROM cliente WHERE codigo=?", cliente.getCodigo());
	}

	/*************************************************************************
	 * En función del argumento realiza una operación de actualización de datos
	 *************************************************************************/
	public int updateHub(String operacion, Cliente cliente) {
		if ( operacion.compareTo( "Insertar") == 0 )
			return insertarCliente( cliente );
		if ( operacion.compareTo( "Actualizar") == 0 )
			return actualizarCliente( cliente );
		if ( operacion.compareTo( "Borrar") == 0 )
			return borrarCliente( cliente );
		return 0;
	}
}

Se puede observar que:


Dentro de la persistencia: los mapper

Un mapper es algo realmente sencillo. Simplemente realiza dos tareas:

 


package com.persistencia.mappers;

import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import com.dominio.Cliente;

/*************************************************************************
 * Mapea atributos del objeto y columnas de la base de datos
 *
 * El metodo mapRow() inyecta en el resultSet los datos del objeto Cliente.
 * Es un callback: llamado por Spring por cada fila del resultSet.
 * Tiene como argumentos dicho resultSet y el nº de fila
 *
 * El método mapAtributos devuelve los atributos del objeto cliente,
 * en la forma de un array de objetos
 *************************************************************************/
public class ClienteMapper implements ParameterizedRowMapper {
	public Cliente mapRow(ResultSet resultSet, int row) throws SQLException {
		Cliente cli = new Cliente();
		cli.setCodigo(resultSet.getString("codigo"));
		cli.setNombre(resultSet.getString("nombre"));
		cli.setApe1(resultSet.getString("ape1"));
		cli.setApe2(resultSet.getString("ape2"));
		cli.setEdad(resultSet.getInt("edad"));
		return cli;
	}
	static public Object[] mapAributos( Cliente cliente) {
		return new Object[] {
				cliente.getCodigo(),
				cliente.getNombre(),
				cliente.getApe1(),
				cliente.getApe2(),
				cliente.getEdad()};
	}
}

Setter

La clase ClientePorEdadSetter es muy breve. Simplemente define los parámetros de una PreparedStament:


package com.persistencia.query;

import java.sql.SQLException;
import org.springframework.jdbc.core.PreparedStatementSetter;
import java.sql.PreparedStatement;

/*************************************************************************
Clase que inyecta parametros en un PreparedStatement
 *************************************************************************/
public class ClientePorEdadSetter  implements PreparedStatementSetter {
	private int edadMinima;
	private int edadMaxima;

	public ClientePorEdadSetter( int edadMinima, int edadMaxima) {
		this.edadMinima = edadMinima;
		this.edadMaxima = edadMaxima;
	}

	public void setValues (PreparedStatement ps) throws SQLException{
		ps.setInt(1, edadMinima);
		ps.setInt(2, edadMaxima);
	}
}

Al implementar el interface PreparedStatementSetter tenemos que definir el método setValues(), que define los parámetros de la sentencia.


La vista: index.jsp y Java Beans

La página principal tiene dos caracteristicas esenciales:


<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ page errorPage="error.jsp"%>
<%@ page import="com.recursos.GestorCarga" %>
<%@ page import="com.persistencia.DAOClienteSpring" %>

<html>

<%@ include file="cabeceraDePagina.jsp" %>

<!-- 	El objeto gestorCarga realiza la carga del contexto (xml),
		propiedades y DAO -->
<jsp:useBean id="gestorCarga" scope="application" class="com.recursos.GestorCarga"/>

<%
	int edadMinima = 25;
	int edadMaxima = 35;
%>

<CENTER><H4><b>Gestión de clientes</H4></b></CENTER>

<!-- INICIO DE TABLA -->
<TABLE BORDER=1 align='center' width='900'>
<TR><TD>

<!--
	El objeto select muestra resultados de consultas de clientes.
	Para ello usa el DAO. Se le pasa un rango de edades para los
	select con where
-->
<jsp:useBean id="select" scope="request" class="com.etiquetas.ClienteEtiquetaSelect">
	<jsp:setProperty name="select" property="dao" value="<%=gestorCarga.getDao()%>"/>
	<jsp:setProperty name="select" property="edadMinima" value="25"/>
	<jsp:setProperty name="select" property="edadMaxima" value="35"/>
</jsp:useBean>

<!-- Listado de todos los clientes -->
<p><b>Listado de clientes: </b><ol>
	<jsp:getProperty name="select" property="select" />
</ol></p>

<!-- Count de todos los clientes -->
<p><b>Número de clientes: </b>
	<jsp:getProperty name="select" property="cuenta" />
</p>

<!-- Listado de los clientes que tienen una edad -->
<p><b>Listado de clientes entre <%=edadMinima%> y <%=edadMaxima%>: </b><ol>
	<jsp:getProperty name="select" property="selectPorEdad" />
</ol></p>

<!-- Count de los clientes que tienen una edad -->
<p><b>Número de clientes entre <%=edadMinima%> y <%=edadMaxima%>: </b>
	<jsp:getProperty name="select" property="cuentaSelectiva" />
</p>

<!-- CAMBIA DE COLUMNA EN LA TABLA -->
</TD>
<TD>

<%@ include file="formulario.jsp" %>

</TD></TR></TABLE>

<p>Las operaciones de Insertar, Actualizar, Borrar y Consultar se refieren 
siempre a un <b>código de registro</b>, que es clave primaria.</p>
<p><b>Actualizar</b>: update del registro cuyo código corresponda con el 
de la página.</p>
<p><b>Consultar</b>: select del registro cuyo código corresponda con el 
de la página.</p>
<p><b>Refrescar</b>: vuelve a presentar la página con listados actualizados.</p>


<%@ include file="pieDePagina.jsp" %>
</html>

Código fuente de formulario.jsp.

El Java Bean "select" se usa para aligerar de Java las páginas JSP. El bean devuelve un String que se vuelca en la página. De esta forma se obtienen los listados de clientes. Más adelante, cuando estudiemos Spring MVC veremos que este uso de los JavaBeans no es necesario; ya que los controladores devolverán a las páginas los objetos que deben mostrar..


package com.etiquetas;
import com.dominio.Cliente;
import com.persistencia.DAOClienteSpring;

/***********************************************************************************
 * JavaBean generador de código html.
 * Es un intermediario entre la página jsp y el dao: la página llama a este JavaBean
 * y el JavaBean devuelve el resultado de las consultas como tipo String
 **********************************************************************************/
public class ClienteEtiquetaSelect {
	DAOClienteSpring dao;			// El DAO
	Integer edadMinima;				// Para un where por edad
	Integer edadMaxima;				// Para un where por edad
	String inicioLinea = "<li>";	// Etiqueta para iniciar linea de select
	String finLinea = "</li>";		// Etiqueta para terminar linea de select

	/***********************************************************************************
	 * Devuelve el nº total de clientes
	 **********************************************************************************/
	public String getCuenta() {
		return ""+dao.getNumeroClientes();
	}
	/***********************************************************************************
	 * Devuelve el nº de clientes entre las edades
	 **********************************************************************************/
	public String getCuentaSelectiva() {
		return ""+dao.getNumeroClientes(edadMinima,edadMaxima);
	}
	/***********************************************************************************
	 * Devuelve todos los clientes. Para separar líneas usa los atributos
	 * incioLinea y finLinea
	 **********************************************************************************/
	public String getSelect() {
		StringBuffer str = new StringBuffer();
		for ( Cliente cliente : dao.selectAll() )
			str.append( inicioLinea + cliente.getCodigo() + ", " +
					cliente.getNombre() + " (" + cliente.getEdad() +")"+finLinea);
		return str.toString();
	}
	/***********************************************************************************
	 * Devuelve los clientes entre las edades. Para separar líneas usa los atributos
	 * incioLinea y finLinea
	 **********************************************************************************/
	public String getSelectPorEdad() {
		StringBuffer str = new StringBuffer();
		for ( Cliente cliente : dao.selectAllPreparedStatement(edadMinima,edadMaxima) )
			str.append( inicioLinea + cliente.getCodigo() + ", " +
					cliente.getNombre() + " (" + cliente.getEdad() + ")"+finLinea);
		return str.toString();
	}
	/***********************************************************************************
	 * Definir periodo de edad
	 **********************************************************************************/
	public void setEdadMaxima(Integer edadMaxima) {
		this.edadMaxima = edadMaxima;
	}
	public void setEdadMinima(Integer edadMinima) {
		this.edadMinima = edadMinima;
	}
	/***********************************************************************************
	 * Definir inicio y fin de linea
	 **********************************************************************************/
	public void setFinLinea(String finLinea) {
		this.finLinea = finLinea;
	}
	public void setInicioLinea(String inicioLinea) {
		this.inicioLinea = inicioLinea;
	}

	public void setDao(DAOClienteSpring dao) {
		this.dao = dao;
	}
}


Volver