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 EnglishAquí 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.
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.
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.
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.
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.
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.
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.
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.
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.