Temario detallado‎ > ‎5: Web Server Side‎ > ‎Java‎ > ‎

Desarrollo Web: Manejo de estado

Repaso Manejo de estado: segundo ejemplo Búsqueda de libros

(la clase está basada en el proyecto libros-ui-jsp-rest que pueden encontrar en los ejemplos)
Cuando seleccionábamos el cliente del celular en la Unidad 2:
  1. lo seleccionábamos de una grilla
  2. el objeto seleccionado estaba directamente bindeado: el control grilla tenía estado y dentro del estado había un selected que era de tipo T, en nuestro caso un Celular.
public void editarCelular(boolean alta) {
                ...
celularAEditar = this.getModel().getSelected();
                ...
Dialog<?> editor = new EditarCelularWindow(this, celularAEditar);
editor.onAccept(new MessageSend(this.getModel(), Search.SEARCH));
editor.open();
}


Ahora en Web, cuando hacemos la búsqueda de un libro,
  1. no tenemos el widget Grid o similar, tenemos que generar una tabla y mostrar en cada columna atributos de algún objeto de dominio. No hay binding bidireccional, ni selecciono una fila, si quiero simular ese comportamiento tengo dos opciones:
    1. agregar un radio button o checkbox o
    2. poner un link <a href>, o un botón. Esto me permite no tener que hacer dos clicks, parece una tontería pero está bueno no tener que hacer dos pasos (seleccionar y dar un click en editar)
  2.  Al no haber grid, y al ser stateless la página, tenemos que asegurarnos que la próxima página reciba toda la información que necesite. Si disparamos el link a una página de detalle y omitimos decirle qué libro es el que está "asociado" a ese link (el "seleccionado"), no vamos a poder mostrar la información de ese libro.
Dos formas de construir la página que muestra la lista de libros con un link:

1) El primer ejemplo guarda en la session la búsqueda de libros (un list de objetos libro) y la página de detalle recibe el índice seleccionado (de 0 a n-1) de la búsqueda de libros: es importante que lo que guardemos en la session sea un List porque necesito garantizar el orden de los elementos. Con eso podemos armar un link a la página detalle.jsp, que pase como parámetro el número de orden:

<a href="detalle.jsp?index=${status.index}">${libro.titulo}</a>

Repasamos Expression Language en el detalle.jsp: ahí podemos acceder al libro mediante ${sessionScope.libros[param.index]}. Una buena idea puede ser asociar el libro a una variable de página (scope page), mediante <c:set>, por ejemplo:

<c:set var="libro" scope="page" value="${sessionScope.libros[param.nro]}"/>

Luego agregamos links al libro anterior y al siguiente usando ${param.index - 1} como index en un <a href="...">Anterior</a>.
como vamos a necesitar utilizar la expresión index - 1 varias veces, definimos otra variable de scope local (local a la página):

<c:set var="anterior" scope="page" value="${param.nro - 1}"/>

Esto es lo más parecido a hacer anterior = nro - 1

Así podemos armar el link a la misma página detalle, pero cambiando los parámetros:
  • el libro anterior va a ser el nro actual - 1
  • la cantidad de libros no se modifica, sigue siendo el size de la búsqueda (¿quién lo definió? el SearchServlet, claro cuando hizo request.getSession().setAttribute("cantLibros", libros.size());)
 <a href="detalle.jsp?nro=${anterior}&cantLibros=${param.cantLibros}">Anterior</a>

También le paso a la jsp de detalle la cantidad de libros, eso permite que no tenga que preguntar el size de la lista de libros (eso lo hace el servlet que puede trabajar más fácilmente código Java).

Vemos cómo se arman los links si corresponden:

Si buscamos el anterior usamos un <c:if> considerando que el anterior tiene que ser mayor a cero
<c:if test="${anterior >= 0}">
    <a href="detalle.jsp?nro=${anterior}&cantLibros=${param.cantLibros}">Anterior</a>
</c:if> 

Para chequear el último no tenemos una forma preestablecida, una propuesta es  agregar un segundo parámetro a la sesión indicando la cantidad máxima y usándola desde ahí. Ah, cierto, era el cantLibros que habíamos mencionado antes:

<c:if test="${siguiente < param.cantLibros}">
    <a href="detalle.jsp?nro=${siguiente}&cantLibros=${param.cantLibros}">Siguiente</a>
</c:if> 

En la session se guarda la búsqueda, la session consume memoria: mientras tengamos una cantidad "razonable" de libros está todo bien. Si la consulta devuelve millones de libros, tendríamos que pensar otra estrategia porque nos va a quedar chica la VM de objetos en memoria.
En resumen:
  • Si dejo el resultado de la búsqueda en el request en la página de detalle tengo que realizar la búsqueda nuevamente. Lo que pasemos por el request a una JSP termina en el cliente: sólo puedo definir <INPUT TYPE="HIDDEN"> para guardar información que necesite luego del lado del servidor, pero esa información tiene que ser un String (recordemos que en HTML puro no tengo objetos).
  • Si dejo el resultado en session, al ingresar por la página principal nos encontramos con que "se acuerda" la última búsqueda (esto no siempre es deseable, hay que evaluar cada caso) 

En la session podemos guardar:
  • información asociada a una "transacción de usuario"
  • pero no queremos guardar objetos que estén ligados a una sesión de base de datos: la naturaleza atómica de la página va en contra de esta idea (no existe el concepto de "conversación", esto lo tenemos que construir nosotros mediante variables session/request)

2) El segundo ejemplo considera poner un identificador entero en libro, modificamos entonces el objeto de negocio por temas de UI. Esto no es algo que el modelo exigiera... esto nos recuerda que es difícil que el negocio no se vea impactado por la tecnología de presentación.

Si la página no tiene estado, ¿cómo puedo saber qué libro tengo seleccionado? Tengo el resultado de la búsqueda (Collection<Libro> en la session), pero en la página no puedo bindear un objeto Libro con una fila de la tabla HTML, ni puedo hacer paginaDetalle.setLibroSeleccionado(this.getModel().getSelected()).

Por eso le pido al modelo que tenga un id que sea unívoco: podría trabajarlo con el título, pero eso no me garantiza unicidad. En objetos tengo garantizada la identidad, se que es "aquel" o "tal" objeto porque tengo una referencia directa, no necesito usar ids, pero si además estoy en un browser que no tiene ambiente, el objeto está "disecado", no tengo ni identidad, ni comportamiento, sólo algunos atributos que elijo mostrar en la jsp. Entonces un id me viene bien...

Incluso vamos a ver que utilizar un id me permite no depender del índice de los objetos encontrados en la búsqueda: no vamos a guardar el resultado de la búsqueda en la session, sino que con el id vamos a recuperar la info del libro directamente.

Vemos la implementación:

a) En la clase Libro agregamos el id

private final int id;

public int getId() {
    return this.id;
}

b) Utilizamos esos ids en los links que van a la página de detalle.

Página index.jsp
<a href="detalleLibro?id=${libro.id}">${libro.titulo}</a>

c) Para procesar ese id ahora necesitamos un servlet, que hace algo así:

>>DetalleServlet.doPost()
int id = Integer.parseInt(request.getParameter("id"));
Libro libro = Biblioteca.getInstance().getLibro(id);
request.setAttribute("libro", libro);

d) Dejamos de guardar las búsquedas en la session. En lugar de guardar la búsqueda nos vamos a guardar los parámetros de búsqueda (en nuestro ejemplo es fácil porque es sólo el título). Además debemos asegurarnos de que esa información pasa en todos los links, lo que no se guarda en el session, debe pasarse por parámetro en cada pedido (esto es lo más complejo).

<a href="detalleLibro?busqueda=${param.titulo}&id=${libro.id}">${libro.titulo}</a>

e) Algunos detalles adicionales: antes ir al siguiente libro era simplemente sumar uno al índice. Ahora necesitamos hacer algo más complejo, realizar nuevamente la búsqueda 

String titulo = request.getParameter("busqueda");
List<Libro> libros = Biblioteca.getInstance().buscar(titulo);

buscar el libro entre sus resultados 

int posicionEnLaLista = libros.indexOf(libro);

calcular los ids de los libros anterior y siguiente en esa búsqueda.

if (posicionEnLaLista > 0) {
    request.setAttribute("idAnterior", libros.get(posicionEnLaLista - 1).getId());
}

if (posicionEnLaLista < libros.size() - 1) {
    request.setAttribute("idSiguiente", libros.get(posicionEnLaLista + 1).getId());
}

f) esos ids ahora los puedo usar en la página de detalle

<c:if test="${requestScope.idAnterior != null}">
    <a href="detalleLibro?busqueda=${param.busqueda}&id=${requestScope.idAnterior}">Anterior</a>
</c:if> 
<c:if test="${requestScope.idSiguiente != null}">
    <a href="detalleLibro?busqueda=${param.busqueda}&id=${requestScope.idSiguiente}">Siguiente</a>
</c:if> 

¿Qué solución es mejor? Ninguna en particular, cada una tiene un criterio diferente para llegar a lo mismo.

 Cuando mantengo en la session los libros encontradosCuando tengo los parámetros de búsqueda en el request y actualizo la búsqueda
 Sólo hago la búsqueda cuando el usuario presiona el botón "Buscar" Hago la búsqueda cuando el usuario presiona "Buscar" pero también cada vez que pido el detalle de un libro. La búsqueda de libros no debería ser costosa de obtener, en caso de que sí lo sea me conviene la otra opción.
 La búsqueda de libros no debería traer muchos objetos, 
  • si esto es así debo analizar la cantidad de usuarios concurrentes (cada uno puede potencialmente traer una gran cantidad de objetos a su sesión)
  • o bien podría no bajar toda la información a la sesión (lo que se conoce como "paginar" la búsqueda, traer sólo los primeros n objetos)
  • o bien debería considerar la otra alternativa
  En este caso no guardo objetos en la sesión, por lo que la VM del servidor no sufre cuando aumenta la cantidad de usuarios concurrentes.
 Manejar el anterior o el siguiente es más fácil, pero hay que conservar el orden de la búsqueda (no podemos guardar en la session una Collection, ni un Set ni un Map, debemos utilizar un List o un SortedSet) No nos preocupa qué devuelve la búsqueda (no agregamos restricciones al negocio), pero encontrar el anterior o el siguiente requiere tenerlo prearmado en el request "por las dudas": por eso cada vez que damos Siguiente la aplicación tiene que volver a calcular el siguiente
 Para borrar la búsqueda y comenzar de cero tenemos que saber cuándo hay que borrar de la sesión la lista de libros    Como no guardamos los resultados de la búsqueda (se calcula todo siempre de cero) aquí no tenemos problemas cuando queremos borrar la búsqueda