Documento actualizado por última vez 13 mayo 2001. Estoy abierto a cualquier sugerencia que pueda ayudarme a mejorar estos materiales. No dudes en escribir al autor, Jonathan Revusky.

This document in English
Ce document en français

Diseccionar el ejemplo del mini-rolodex

Aquí suponemos que ya has compilado y ejecutado el servlet, como se explica aquí. Una vez has pasado por esas etapas mecánicas, probablemente querrás un poco de ayuda para comprender los elementos que componen este ejemplo.

El API de Datos de Niggle

Si este ejemplo representa un gran salto hacia delante comparado con el ejemplo anterior, es porque este servlet guarda y recupera sus datos de aplicación desde un repositorio persistente. Aun y así, quizá te sorprende que este ejemplo no requiere mucho más código que el ejemplo anterior. Esto es debido a su uso de los API's que proporciona niggle para la persistencia de datos. Esto nos permite mantener todos los "detalles sucios" bien encapsulados en ficheros externos en XML.

Los API's de Niggle para datos persistentes que introducimos se hallan en el package com.revusky.niggle.data. Hay 3 abstracciones importantes que introducimos en este ejemplo: com.revusky.niggle.data.MutableDataSource, com.revusky.niggle.data.Record, y com.revusky.niggle.data.DataRegistry.

Aunque existen varias terminologías que provienen de la informática y la teoría de bases de datos, es de esperar que la mayoría de programadores se sentirán cómodos con la terminología que hemos adoptado: un mutable data source es un objeto que nos permite guardar, recuperar, y modificar records, o mejor dicho, registros en castellano. A su vez, un record o registro es básicamente lo mismo que una línea en una tabla de una base de datos relacional. El tercer objeto mencionado arriba, el data registry, encapsula los metadatos del sistema. Nos permite buscar las fuentes de datos disponibles por nombre. También nos permite obtener una nueva instancia de un cierto tipo de record, para poder llenarlo con datos. Fíjate que estos metadatos son definidos en los ficheros XML, recorddefs.xml and datasources.xml. Entonces, el objeto com.revusky.niggle.data.DataRegistry realmente es el puente entre la parte que es código java y la especificación externa en XML.

Manipular los Registros

Intentemos orientarnos mirando el código java para ver dónde se usan esos nuevos API's que acabo de mencionar.

En este ejemplo, el código que recupera los registros se halla en cuatro métodos, getData(), getAllEntries(), getEntry() y newEntry().

Echa una ojeada al método getData. Sólo contiene dos líneas. La primera línea obtiene la instancia (sólo hay una, es un singleton) de nuestro objeto tipo DataRegistry. Esto nos permite buscar nuestro DataSource por nombre. Fíjate que el nombre de "rolodex_data" es bastante arbitrario. Lo que pasa es que es el nombre de la fuente de datos que especificamos en el fichero externo, datasources.xml. Entonces, getData() nos pesca la referencia al DataSource que usamos en este servlet.

El método getAllEntries() devuelve todos los registros en el DataSource en un contenedor de tipo java.util.List. Lo hace invocando el método select() de nuestro DataSource con un argumento nulo. Normalmente, este método recibe un argumento de tipo com.revusky.niggle.data.RecordFilter que indica un subconjunto de registros para recuperar. Pero si pasamos null como parámetro, como hacemos aquí, la lista que se devuelve contiene todos los registros de la fuente de datos.

El método getEntry() es algo más complicado. El método supone que la clave primaria (primary key) del registro con que trabajamos está encrustado en la petición HTTP por medio de unique_id=. Si no hay ninguna entrada disponible de esta manera, el método devuelve una nueva "entrada virgen" por medio del método newEntry(). Lo que este último método hace es que pide una nueva entrada al repositorio de datos -- es decir, nuestra instancia de com.revusky.niggle.data.DataRegistry -- indicando el tipo de registro que interesa, en este caso, "rolodex_entry". Fíjate que esto funciona porque los metadatos de "rolodex_entry" son especificados en el fichero recorddefs.xml.

Tratamiento de Peticiones y Presentación

Es interesante comparar y contrastar este servlet con el ejemplo anterior, el libro de invitados. Por ejemplo, los dos ejemplos contienen un método que se llama execEntries() y, de hecho, los dos son casi idénticos. La diferencia está en que este ejemplo aprovecha la capa de persistencia que ofrece Niggle para obtener la lista de entradas a exponer al mecanismo de plantilla de página. En el ejemplo anterior, todos los elementos de la lista eran simplemente instancias de java.util.Hashtable Pero aquí son instancias de com.revusky.niggle.data.Record. Es importante fijarte aquí que los registros Niggle pueden ser expuestos al mecanismo de plantilla de exactamente la misma manera que un Hashtable (o cualquier objeto que implemente java.util.Map). El código aprovecha esta prestación también en el método execEdit().

Hay otro aspecto que hace que este ejemplo sea más completo que el anterior. Mientras el libro de invitados sólo permitió inserción, este ejemplo ofrece la posibilidad de modificar y de borrar entradas. Entonces, el método execProcess() es algo más complejo y existe también un método execDelete()

Miremos primero el método execDelete() ya que es algo más sencillo. En la primera línea, el código extrae el parámetro unique_id= de la petición. Entonces, pesca el registro correspondiente. La próxima línea invoca el método delete() del DataSource, que toma la clave primaria del registro como parámetro. Despues de eso, se define la variable page, que representa la plantilla de página. Esto corresponde a "ack.nhtml" que se usa para reconocer que la operación ha tenido lugar. Fijamos la variable booleana deleted para indicar que esta operación fue la de borrar un registro.

El método execProcess() que procesa la página de edición, es algo más compleja. El motivo por eso es que debe distinguir entre la inserción de una entrada nueva y la modificación de una entrada ya existente. Como puedes ver, recupera la entrada con la cual operar por medio del método getEntry(). Ahora, getEntry() devuelve una de dos cosas: una nueva "entrada virgen" (si no hubo ningun parámetro tipo unique_id= con que trabajar) o el registro existente que hay que modificar. El modo en que distingue entre los dos casos es el método isImmutable(). Lo que hay que comprender es que un registro nuevo en Niggle se crea en un estado mutable. Cuando se introduce en un contenedor tipo DataSource, se vuelve inmutable. Para luego modificar ese registro, lo que hacemos de verdad es que creamos un clónico mutable llevamos a cabo las modificaciones, y después lo sustituimos. Algunos lectores se darán cuenta que esta disposición que puede parecer algo rebuscada tiene que ver con hacer garantías sobre "thread safety", es decir la integridad de datos en un entorno multihilo. Luego desarrollaremos esas ideas. En este momento, lo que hay que saber es que el acto de modificar un registro existente es fundamentalmente distinto al acto de insertar una nuevo. Es por eso que, con un registro existente, tenemos que hacer la llamada a getMutableCopy antes de llenar los campos desde los parámetros de la petición.

El método DataUtil.fillRecordFromServletRequest() es un método muy cómodo que usa los metadatos del registro para iterar por los campos del registro y llenarlos basándose en los parámetros de la petición HTTP. Después de este método, invocamos el insert() o update() del DataSource, dependiendo de si se trata de una entrada nueva o la modificación de una existente. Finalmente, después de llevar a cabo nuestras modificaciones de datos, sacamos la plantilla de página que usamos para dar constancia que la operación tuvo lugar. Exponemos la entrada a la página que se interpreta como una variable tipo correspondencia.

Recuperarse de Errores

La última novedad de este ejemplo es el método recover(). Se trata de un "gancho" que podemos implementar para darnos un lugar cómodo donde recuperar de errores. Por ejemplo, si, en nuestra configuración de metadatos, definimos un campo ("last name" por ejemplo) como obligatorio, y el usuario no lo rellena, el motor de persistencia lanzará una excepción de tipo com.revusky.niggle.data.MissingDataException. El método recover() nos da una oportunidad de tratar estas condiciones de modo suave.

El fichero de configuración de los DataSource

Ahora puedes echar una ojeada a los ficheros de configuracion en XML. Empecemos mirando datasources.xml. Esto no debería presentar mucho misterio, incluso si nunca has trabajado con XML. El XML se usa aquí como formato de configuración externa legible por humanos. Esto se hace para mantener los detalles de la capa de persistencia totalmente separados lógicamente del código de la aplicación.

Se trata del empleo más minimalista de un fichero de estos ya que sólo define un DataSource, al cual asignamos el nombre "rolodex_data".

El atributo CLASS dice que es la implementación concreta que usamos. Usamos com.revusky.niggle.data.inmemory.InMemoryRecordSet. Esta clase es una implementación de la interfaz com.revusky.niggle.data.MutableDataSource que mantiene los datos en memoria y los guarda en un fichero de texto plano. Es muy útil para desarrollo. Y de hecho, esta implementación se puede usar para proyectos hasta una escala moderada. En algun momento, siempre se puede convertir la aplicación a usar una DATASOURCE CLASS que es una fachada delante de una base de datos externa como Oracle o lo que sea. Y se puede conseguir eso básicamente cambiando unas cuantas líneas de XML. Ya que el código java no sabe que implementación está detrás, sigue funcionando igualmente sin cambios.

De todas formas, como indica la terminología, los registros en un InMemoryRecordSet se mantienen en memoria. Se escriben a un fichero de texto plano. Las dos propiedades que definimos, STORE y PERSIST_FREQUENCY, configuran la ubicación del fichero plano, y la frecuencia con que todo el fichero se reescribe desde cero. Si hubieses configurado un DataSource tipo JDBC aquí (mira datasources2.xml) probablemente tendrías que fijar algunos parámetros más, pero esos detalles los dejamos para luego. Es importante comprender que los detalles del mecanismo de persistencia de datos son completamente transparentes tanto al código java de aplicación, como a la capa de presentación, las plantillas de página.

Definir los metadatos de registros

Ya es hora para mirar la otra parte del rompecabezas, los metadatos de los registros. Si miras el fichero recorddefs.xml, verás que define exactamente un tipo de registro, que se llama "rolodex_entry".

El primer campo de nuestra definición de registro es el campo "unique_id", que también es la clave primaria o primary key. También ocurre que nuestra clase DataSource asignará una clave única al azar cada vez que se inserte un registro nuevo.

Los otros campos definidos aquí son todos cadenas. Algunos son obligatorios, es decir "required" y otros no. Algunos de los campos incluyen normalizaciones que ofrecen alguna información adicional al sistema niggle sobre cómo limpiar los datos cuando se leen de la interfaz de usuario. De todas formas, probablemente será obvio que significa que un campo es "required" o bien "capitalized" jugando con el servlet.

Conclusiones

Este ejemplo ya introduce casi todos los elementos conceptuales de una aplicación más compleja basada en la librería Niggle. Si consigues ponerte cómodo con todos estos elementos, habrás superado los principales obstáculos en aprender Niggle. Hay más prestaciones por supuesto, y más aparecerán con el tiempo, ya que Niggle es un proyecto vivo en que trabajamos aun. Sin embargo, las cosas que aprendes de este punto en adelante serán de naturaleza más bien incremental.

Anexo: Cómo usar MySQL u otra BD en vez de ficheros planos

Fíjate que el fichero datasources2.xml contiene un fichero alternativo que usa una BD externa para persistencia. Para usar esto, seguramente tendrás que cambiar algunos de los parámetros.

En este momento, sólo he verificado que esto funciona con MySQL. No veo ningún motivo para que no funcione con otra BD, mientras ésta dispone de un driver JDBC. Sin embargo, lógicamente, ¡estaría muy interesado en obtener alguna confirmación de esto! Si has conseguido hacer que este ejemplo funcione con otra BD, o bien, si lo has intentado y has tenido problemas ¡por favor escríbeme para contar tu experiencia!

De todas formas, un buen modo de proceder será conseguir que tu lógica de aplicación funcione con los ficheros planos, ya que no require ninguna configuración de herramientas externas, y luego cambiar a una BD externa cuando sea necesario hacerlo.