Documento actualizado por última vez 28 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.
Hay dos objetos muy importantes que introducimos
en este ejemplo: com.revusky.oreo.MutableDataSource
y com.revusky.oreo.Record
.
Aunque existen varias terminologías en la informática y la teoría de las bases de datos, es de esperar que la mayor parte de los programadores de aplicaciones se sientan 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. A veces también se usa la terminología de tuple o entity para decir lo mismo.
Al igual que una línea en la tabla de una base de datos, un registro Oreo representa simplemente un conjunto de pares clave/valor que obedecen a un cierto esquema de metadatos, es decir una descripción de los nombres de las claves y las restricciones que rigen los valores asociados con ellas. Es eso lo que queremos decir cuando hablamos de los campos de un registro. Todos los registros de un cierto tipo comparten los mismos metadatos -- dicho de otra forma, tienen la misma disposición de campos. Aparte de eso, se supone que para cada tipo de registro, exactamente uno de los campos es el campo de la clave primaria. La combinación del tipo de registro y el valor de la clave primaria deben servir para identificar un registro sin ambigüedad a la hora de guardar or recuperarlo de un objeto tipo DataSource.
Si aun no lo has hecho, echa una ojeada al fichero recorddefs.xml. Esto parecerá más o menos asequible, todo depende de tu experiencia previa con el XML. No hay motivo para asustarse. Usamos XML solamente como un formato de fichero de configuración que pueda leer los humanos. Cada fichero XML representa una jerarquía de elementos con una única raíz. El elemento raíz en este caso es RECORDDEFS, que, a su vez, contiene uno o más elementos de tipo RECORD. Ya que aun se trata de un ejemplo minimalista, sólo definimos un tipo de registro, del tipo rolodex_entry. La PRIMARY_KEY (clave primaria) es el campo unique_id. Ese campo, el primer campo definido dentro del elemento RECORD, es definido como un campo numérico, de tipo INTEGER, y es no negativo, ya que la propiedad MINIMUM es igual a cero. Los próximos dos campos, first_name y last_name son campos tipo cadena. Fíjate que, además de ser especificados como REQUIRED (obligatorio) también pueden disponer de una o más directivas tipo NORMALIZE. Esto puede ser algo útil que especificar, ya que especifican ciertos tipos de "limpieza" a hacer a la hora de leer el valor de un campo desde una interfaz de usuario. Por ejemplo, no quieres realmente tratar la cadena "Smith " como algo distinto de "Smith" o incluso " SMITH". Los dos NORMALIZE nos permiten decir que queremos tratar todas las cadenas mencionadas como iguales, lo cual, desde el punto de vista humano (a diferencia del punto de vista ordenador) lo son sin duda.
El próximo campo, email también es REQUIRED, es decir obligatorio, y es de tipo email. Esto es en realidad una pequeña comódidad que la librería proporciona que te permite verificar automáticamente si un email es bien formado. Los próximos campos son opcionales.
Verás que el fichero datasources.xml es bastante breve. Hay dos propiedades de configuración: STORE y COMPACT_FREQUENCY. El STORE especifica simplemente donde se debería guardar los datos. Esto debería ubicarse en algun sitio donde tu proceso puede leer/escribir. El COMPACT_FREQUENCY es un parámetro algo técnico que también es opcional, ya que, si no lo especificas, se usará un valor por defecto. Esto dice con qué frecuencia debemos volver a escribir todo el fichero de datos desde cero en una forma más compacata. Con un valor de 100, esto quiere decir que 99 de cada cien veces que escribes al disco sólo será para agregar información al final del fichero. Y después de 100 veces, se vuelve a escribir el fichero en una forma más compacta.
Hay algo que vale la pena acordarse mientras vamos hacia adelante para mirar el código java. En los dos ficheros de configuración XML, definimos un tipo de registro y una fuente de datos. El tipo de registro se llama "rolodex_entry" y la fuente de datos se llama "rolodex_data". ¡Estos son los nombres que usaremos para poner nuestras sucias manos encima en el código java!
Pues está todo muy bonito especificar cosas en un formato bien definido, pero el hecho de hacerlo no hace nada en sí. Ya es hora de mirar dónde hacemos algo con todo eso en el código java.
Mira el fichero MiniRoloServletInteraction.java. Este servlet minirolo es bastante sencillo. Su funcionalidad se puede resumir por 4 acciones de base:
Las acciones mencionadas arriba corresponden a los métodos: execDefault()
, execDelete()
,
execEdit()
, y execProcess()
respectivamente.
Empecemos mirando el método execDefault()
. Este método
proporciona la vista por defecto, que es simplemente una relación
de todas las entradas. Es sorprendementemente breve.
Lo que hace es que empieza obteniendo una referencia a la fuente
de datos que definimos. Luego, invoca el método select()
del objeto DataSource con un argumento nulo. Debería mencionar
que el método select()
normalmente recibe un parámetro
de tipo com.revusky.oreo.RecordFilter
el cual implica
un subconjunto de registros a recuperar de la fuente de datos.
Pero si, como en este caso, pasamos un argumento nulo, la lista que
devuelve contendrá todos los registros de la fuente de datos.
Reconocerás la línea después si pasaste por los tutoriales anteriores. Simplemenete pescamos la plantilla de página apropiada para mostrar las entradas. Luego, en la última línea, simplemente exponemos la lista de golpe. Si quieres, puedes mirar el fichero entries.nhtml ahora para ver el último cabo suelto de este truco de magia.
Ahora miremos cómo el método execDelete()
trata
la acción delete para borrar un registro. Empieza de la misma manera
que execDefault()
. Obtiene una referencia a nuestra única
fuente de datos, la cual tiene un nombre de
"rolodex_data". Pues la acción de borrar se refiere a borrar
una entrada específica de una fuente de datos. ¿Te acuerdas de que dije
arriba que la combinación de tipo y clave primaria debe identificar
sin ambigüedad un registro en una fuente de datos? Bueno, ahora lo ponemos
en la práctica. Bueno, este código supone que el valor de la clave
primaria está encrustado en la petición de servlet. Ya que esto se hace
también en otras partes del código, el código para obtener el unique_id
está en su propio método pequeño que se llama getEntryID()
, el cual
devuelve un Integer (o null) basándose en el parámetro
unique_id en la petición del servlet.
Ya que tenemos un id y una fuente de datos, podemos recuperar
el registro en cuestión usando el método
get()
del objeto tipo MutableDataSource. Suponiendo
que el registro que nos devuelve no es nulo, podemos invocar el método delete()
para borrar el registro de la fuente de datos.
Ahora lo único que nos queda por hacer es dar algún acuse al cliente de que la operación ha sido llevada a cabo. Pescamos la plantilla apropiada, que en este caso se llama ack.nhtml y exponemos alguna información encima. Por ejemplo, una variable que indica que la operación en cuestión ha sido borrar. Y también exponemos el registro que quitamos de la fuente de datos por si acaso el usuario quiere echarle una ultima ojeada. (Fíjate que es el tipo de esquema que se aplica ampliamente a diversos tipos de sitios de comercio electrónico.)
El código de la acción edit no es muy distinto realmente del código que acabamos de mirar para borrar. Es un poquitín más complicado ya que usamos esta misma acción por lo que son realmente 2 casos distintos:
De hecho, es sólo en el segundo de los casos arriba que necesitamos la clave primaria. Bueno, lo que hace este código es que busca una clave primaria en los parámetros de la petición (al igual que hicimos para borrar) pero si encuentra una, supone que el usuario quiere modificar la entrada correspondiente. Pero si no hay ningun parámetro unique_id= en la petición, se supone que lo que el usuario quiere hacer es agregar una entrada nueva.
Ahora, sería una buena idea echar un vistazo a la plantilla de página edit.nhtml para ver como todo eso funciona en su conjunto.
Aunque tampoco es muy largo, el método execProcess()
es el más
complicado de los 4. El motivo principal que explica esto es que tiene
que distinguir entre dos casos, si alguien está agregando una entrada nueva
o modificando una entrada ya existente. Al igual que en el caso anterior,
lo hace basándose en si la petición contiene un paramétro "unique_id="
encrustado dentro. El primer caso que el código trata es el caso donde
no lo, y por lo tanto, se trata de una entrada nueva.
En este método hay un par de cosas en que vale la pena hacer hincapié.
La primera es que hay un método de utilidad fillInFields()
que
rellena bastante mágicamente los valores de los campos del registro basándose
en los parámetros de la petición HTTP. Una vez invocamos ese método,
el esquema es más o menos igual. Llamamos insert()
o bien update()
,
depende de si se trata de una entrada nueva o una modificación. Finalmente,
después de llevar a cabo las modificaciones de datos, pescamos una plantilla
de página para reconocer que la operación ha sido llevada a cabo. Luego,
exponemos el registro, que se interpreta como variable tipo correspondencia
en la capa de presentación.
La segunda cosilla en que vale la pena prestar atención es que una diferencia básica
entre el primer caso y el segundo caso que trata este método es la invocación del
método getMutableCopy
en el caso en que estamos modificando un
registro ya existente. Ves, un registro Oreo nuevamente creado está en un
estado mutable. Una vez lo ponemos en una fuente de datos, se vuelve
inmutable. Para modificar ese registro, lo que hacemos es, creamos un clónico
mutable, llevamos a cabo las modificaciones y entonces, sustituimos el viejo registro
con el nuevo. No sé cuantos lectores se darán cuenta en este momento que esta
disposición está basada en garantizar seguridad en un entorno multihilo. Bueno,
desarrollaremos esa idea en algún momento luego. Por el momento, lo que tienes
que saber es que el acto de modificar un registro ya existente es fundamentalmente
distinto del acto de agregar uno nuevo. El código de este método es la primera
(pero no la última) vez que toparás con esto.
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.oreo.MissingDataException
.
El método recover() nos da una oportunidad de tratar estas condiciones
de modo suave.
Es interesante comparar y contrastar este servlet
con el ejemplo anterior, el libro de invitados. Por ejemplo,
los métodos execDefault()
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.oreo.Record
.
Es importante fijarte aquí que los registros Oreo 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()
.
Otro aspecto que hace que este ejemplo sea más complejo
que el libro de invitado es que, mientras el libro de invitados
sólo permitía agregar entradas, este ejemplo también te permite
modificar y borrar. Entonces, el método execProcess()
es
más complejo, ya que debe tratar los dos casos de forma distinta.
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.
Ya que este ejemplo es sencillito, debería funcionar con casi cualquier base datos si esta dispone de un driver JDBC. Tendrás que cambiar los parámetros JDBC_URL y JDBC_DRIVER_CLASS por supuesto. Sin embargo, este tipo de cosa tiende a ser un poco complicado. Si has conseguido hacer que el ejemplo funcione con otra BD o has topado con 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.