Document mis à jour le 28 mai 2001. Je suis ouvert à n'importe quelle suggestion qui pourrait aider à améliorer ces matériaux. N'hésitez pas d'écrire à l'auteur, Jonathan Revusky.

This document in English
Este documento en español

Eplucher l'exemple du mini-rolodex

Rendu ici, on suppose que vous avez déjà fait fonctionner l'exemple, comme on explique ici. Une fois que vous avez franchi ces démarches mécaniques, vous voudrez peut-être un peu d'aide afin de mieux comprendre les éléments qui composent cet exemple.

L'API de Données d'Oreo

Si cet exemple représente un grand saut relatif à l'exemple antérieur, c'est parce que ce servlet garde et récupère les données de l'application d'un répositoire persistant. Tout de même vous resterez peut-être étonné en voyant que cet exemple ne requiert pas beaucoup plus de code que l'exemple antérieur. Cela est dû à l'usage des API's que Niggle fournit pour la persistance de données. Cela nous permet de garder les "détails grossiers" bien encapsulés dans des fichiers externes en XML.

Les API's d'Oreo de données persistantes qu'on introduit se trouvent dans le package com.revusky.oreo. Il y a 2 abstractions importantes dont on se sert ici: com.revusky.oreo.MutableDataSource et com.revusky.oreo.Record.

Bien qu'il existe de diverses terminologies enracinées dans l'informatique et la théorie des bases de données, on espère que la plupart des programmeurs se sentent confortables avec la terminologie qu'on a adoptée ici: un mutable data source, c'est un objet qui nous permet de garder, recupérer, et modifier des records -- ou bien, des enregistrements en français. Un record, c'est la même chose conceptuellement qu'une ligne dans une table d'une base de données relationelle. Parfois on l'appelle aussi un tuple ou bien entity.

Le Record ou Enregistrement chez Oreo

Tout comme une ligne d'une table dans une base de données, un enregistrement dans l'API Oreo représente un ensemble de paires clé/valeur régi par un schème de metadonnées -- c'est à dire une description des noms des clés et les restrictions sur les valeurs associées. C'est ce qu'on veut dire quand on fait mention des champs d'un enregistrement. Tous les enregistrements d'un type donné ont les mêmes metadonnées -- c'est à dire, la même disposition de champs. D'ailleurs, on suppose que, pour chaque type d'enregistrement, exactement un de ses champs est associé à la clé primaire de l'enregistrement. La combination du type d'enregistrement et la valeur de sa clé primaire devrait l'identifier sans ambiguité pour le garder/récupérer dans un objet de type MutableDataSource.

Si vous ne l'avez pas encore fait, regardez le fichier recorddefs.xml. L'effet que ça vous fait dépendra de votre expérience antérieure avec le XML. Il n'est pas besoin d'être intimidé. On se sert de l'XML comme format de fichier de configuration lisible par des humains. Bref, chaque fichier XML, c'est un hiérarchie d'éléments avec une seule racine. L'élément racine en ce cas ci, c'est RECORDDEFS, qui contient un ou plus d'un élément du type RECORD. Etant donné que c'est un petit exemple, on ne définit qu'un seul type d'enregistrement, appelé rolodex_entry. La clé primaire, indiquée par PRIMARY_KEY, est le champ unique_id. Ce champ, le premier défini dans l'élément RECORD, est défini comme champ numérique, du type INTEGER, qui doit être non-négatif, vu que le MINIMUM, c'est zéro. Les deux prochains champs, first_name et last_name sont des chaînes. Remarquez que, à part le fait d'être REQUIRED (obligatoires), ils ont aussi quelques directifs NORMALIZE associés. Ça peut être très utile spécifier ces choses, parce que ça vous permet de "nettoyer" au moment de lire des valeurs d'une interface d'usager. Par exemple, vous ne voudrez pas vraiment traiter la chaîne "Smith " comme quelque chose de différent de "Smith" tout juste ou même "SMITH". Les deux directifs de NORMALIZE ici nous permettent de dire qu'on voudrait traiter toutes les chaînes ci-dessus comme étant la même, une disposition bien évident du point de vue humain (mais pas si evident du point de vue ordinateur.)

Le prochain champ, email, est obligatoire aussi, comme on voit. On le spécifie comme type "email". C'est une astuce que la librairie fournit pour la verification automatique qu'un email est bien formé. Les prochains champs son optionnels.

Bon, je crois que ça nous dit tout ce qu'il faut savoir pour le moment au sujet des définitions des enregistrements. Maintenant, on passe à la configuration des sources de données.

Les Sources de Données chez Oreo

Si vous regardez le fichier datasources.xml, vous verrez que c'est assez bref aussi. On définit une seule DATASOURCE et il n'y a que deux proprietés de configuration en ce cas-ci: STORE et COMPACT_FREQUENCY. La proprieté STORE spécifie tout simplement un fichier où on devrait garder les données. Evidemment, ça devrait être un endroit où votre servlet a le droit de lire/écrire. COMPACT_FREQUENCY est un paramètre de nature plutôt technique. C'est optionnel, puisque si on ne le spécifie pas, le système utilisera une valeur par défaut. Ce paramètre fixe la fréquence avec laquelle votre fichier devrait être écrit à nouveau depuis zéro. Avec une valeur de 100 spécifiée, ça veut dire que 99 de 100 écritures sur disque ne seront que des écritures qui ajoutent à la fin du fichier. Et la centième fois, ça écrira le fichier à nouveau d'une forme plus compacte.

Voici quelque chose qu'il vaut la peine de se rappeler lorsqu'on passe au code java. Dans ces deux fichiers de configuration en XML, on a défini un type d'enregistrement record et une source de données. Le nom du type d'enregistrement est "rolodex_entry" et le nom de la source de données, c'est "rolodex_data". C'est par moyen de ces noms qu'on pourra mettre nos sales mains dessus dans le code java!

Faire des Trucs avec les Enregistrements

D'accord, il est bel et bien de spécifier les choses avec un format bien défini, mais quand-même, on n'a rien fait jusqu'ici. Maintenant c'est le moment de regarder comment on se sert de tout ça dans le code java.

Jettez un coup d'oeil au fichier MiniRoloServletInteraction.java. A vrai dire, le servlet minirolo est assez simple. On peut caractériser sa fonctionnalité par les 4 actions de base suivantes:

  1. Montrer tous les enregistrements. (Il s'agit de l'action default.)
  2. Effacer un enregistrement. (Il s'agit de l'action delete.)
  3. Donner à l'usager une page avec un formulaire pour introduire/modifier un enregistrement. (Il s'agit de l'action edit.)
  4. Traiter les résultats du formulaire. (Il s'agit de l'action process.)

Les actions mentionnées ci-dessus correspondent aux méthodes execDefault(), execDelete(), execEdit() et execProcess()respectivement.

L'action Default: Montrer toutes les Entrées

Commençons en regardant le code du méthode execDefault(). Il s'agit du méthode qui fournit la vue par défaut, qui est simplement une liste de toutes les entrées du système. C'est peut-être étonnant de voir jusqu'à quel point c'est bref. Ce qu'il fait, c'est qu'il commence en obtenant une référence à la source de données qu'on a définie das l'étape précédente. Donc, dans la ligne suivante, il invoque le méthode select sur l'objet DataSource avec un argument nulle. Je devrais mentionner en passant que select() prend généralement un argument du type com.revusky.oreo.RecordFilter qui implique un sous-ensemble d'enregistrements à récupérer d'une source de données. Mais si, comme dans ce cas-ci, on lui donne un argument null, la liste rendue contiendra simplement tous les enregistrements de la source de données en question.

Vous devriez reconnaître la ligne suivante si vous avez passé par les tutoriels précédants. Ce qu'on fait, c'est qu'on obtient la page modèle dont on va se servir pour montrer nos entrées. Donc, la ligne suivante expose la liste d'un seul coup. Rendu ici, il serait peut-être une bonne idée de regarder le fichier entries.nhtml afin de voir où se passe toute la "magie".

L'Action Delete

Maintenant, regardons comment le méthode execDelete() traite l'action d'effacer un enregistrement. En fait, il commence de la même façon que le méthode execDefault() qu'on vient de regarder ci-haut. Il obtient une référence à notre seule source de données, qui s'appelle "rolodex_data". Bon, l'action d'effacer veut dire qu'il faut enlever une entrée spécifique de la source de données où elle se trouve. Vous vous rappelez de ce qu'on a dit avant? La combination du type d'enregistrement et la clé primaire devrait indentifier un registrement d'une source de données. Bon, ce qu'on suppose ici, c'est que la clé primaire unique_id est incrustée dans la pétition HTTP. Vu qu'on se sert de cette même disposition ailleurs, on met le petit bout de code pertinent dans son propre méthode, method, called getEntryID(), qui nous donne un Integer (ou null) en se basant sur le paramètre unique_id qui se trouve dans la pétition.

Maintenant, vu qu'on dispose d'une clé unique et une source de données, on peut obtenir l'enregistrement en question en utilisant le méthode get() de l'objet type MutableDataSource. Tout en supposant qu'il nous rend un enregistrement non-nulle, on peut invoquer le méthode delete() pour enlever cet enregistrement du conteneur.

Ce qu'on fait finalement, c'est qu'on envoit quelque message de confirmation à l'usager. On obtient la page modèle ack.nhtml et on expose l'information dont elle a besion. On expose expose une variable qui indique que l'opération effectuée a été d'effacer. D'ailleurs, on expose l'enregistrement qui a été enlevé du conteneur, au cas où le client pourra y jetter un dernier coup d'oeil sur la page de confirmation. (En fait, c'est le genre de disposition qui s'applique à toute sorte de site de commerce electronique typique.)

L'Action Edit

A vrai dire, le code qui traite l'action edit n'est pas tellement différent du code qu'on vient de voir. La différence principale, c'est qu'on se sert de la même action pour 2 cas différents:

On a besoin de chercher l'enregistrement par clé primaire seulement dans le second des deux cas ci-dessus. Essentiellement, ce qu'on fait, c'est qu'on cherche une clé primaire dans les paramètres de la petition HTTP (exactement comme dans le cas où il s'agissait d'effacer un enregistrement) et si on la trouve, on suppose que l'usager veut modifier l'entrée correspondante. S'il n'y a pas de paramètre unique_id= dans la pétition, on suppose que l'usager veut ajouter une nouvelle entrée.

Maintenant, je vous encourage de jetter un coup d'oeil à la page modèle edit.nhtml afin de mieux voir la façon dont tous les éléments travaillent ensemble.

L'Action Process

Tout en étant assez bref tout de même, execProcess() est le plus compliqué des 4 méthodes execXXX de cet exemple. C'est dû surtout au fait qu'il doit distinguer entre deux cas, si quelqu'un ajoute une nouvelle entrée ou modifie une entrée existante, et il doit les traiter séparemment. Encore une fois, c'est basé sur la présence ou absence du paramètre "unique_id=" dans la pétition HTTP. Le premier cas traité, c'est où le paramètre est absent, et donc, il s'agit d'une nouvelle entrée.

Il y a deux choses importantes à remarquer dans ce méthode. La première, c'est le méthode d'utilité fillInFields() qui remplit automatiquement les champs d'un enregistrement en se basant sur les paramètres dans la pétition HTTP. Il le fait en se servant des métadonnées de l'enregistrement pour itérer a travers ses champs et les remplir en se basant sur les paramètres de la pétition HTTP. Dès qu'on fait ça, il s'agit d'invoquer ou bien insert() ou update() sur la source de données, dépendant de si c'est une entrée nouvelle ou une modification. Finalement, après avoir effectué les modifications des données, on obtient la page modèle appropiée pour confirmer que l'opération a été effectuée et donc, on expose l'enregistrement qui est interpreté comme une variable type corréspondance dans la couche de présentation.

A part la disposition ci-dessus, l'autre chose importante à remarquer, c'est la différence de base entre le premier est le deuxième cas traité. Remarquez la présence de l'appel au méthode getMutableCopy dans le cas où on modifie un enregistrement qui existe déjà. Vous voyez, un enregistrement Oreo qui vient d'être créé est dans un état mutable. Dès qu'on le met dans un conteneur type MutableDataSource, il est validé et devient immutable. Bon, pour modifier cet enregistrement, ce qu'on fait, c'est qu'on créé un clone mutable, on y effectue les modifications et ensuite, on sustitue le nouvel enregistrement pour l'ancien. Bon, je ne sais pas combien de lecteurs se rendront compte tout de suite que toute cette disposition -- qui, à première vue peut paraître quelque peu baroque -- a à voir avec des garanties de sécurité dans un environnement multitâche. On développera ces idées en plus de détail ailleurs. Rendu à cette étape, il faut simplement retenir que l'acte de modifier un enregistrement existant est fondamentalement différent de l'acte d'ajouter un enregistrement nouveau. Le code de ce méthode peut être la première fois que vous voyez ceci, mais ce ne sera pas la dernière, au moins si vous continuez d'utiliser cette librairie!

Récupération d'Erreurss

La dernière nouveauté de cet exemple se trouve chez le méthode recover(). Il s'agit d'un méthode qu'on peut redéfinir et qui nous fournit un endroit pour récupérer d'erreurs. Par exemple, si, dans notre configuration de métadonnées, on définit un champ ("last name" par exemple) comme étant obligatoire, et l'usager ne le remplit pas, la moteur de persistance sous-jacente lancera une exception du type com.revusky.oreo.MissingDataException. Le méthode recover() nous fournit une opportunité de traiter ces conditions "doucement" disons.

Conclusions

Ça peut être très instructif de comparer et contraster ce servlet avec celui de l'exemple antérieur, le livret d'invités. Par exemple, le méthode execDefault() est presque le même dans les deux cas. La différence clé, c'est que cet exemple exploite la couche de persistance que Niggle fournit pour obtenir la liste d'entrées à exposer au mécanisme de pages modèles. Dans l'exemple antérieur, tous les éléments de la liste n'étaient que des instances de java.util.Hashtable. Mais ici, ce sont des instances de com.revusky.oreo.Record. C'est important de bien remarquer que les enregistrements Oreo peuvent être exposées au mécanisme de pages modèles de la même façon qu'un Hashtable (ou n'importe quel objet qui implémente java.util.Map.) Ce code profite de cette prestation aussi dans le méthode execEdit().

Il y a un autre aspect qui rend cet exemple plus complet que l'antérieur. Tandis que le livret d'invités ne permettait que l'insertion, cet exemple offre la possibilité de modifier et d'effacer les entrées. Donc, le méthode execProcess() est un peu plus complèxe et il existe aussi un méthode execDelete() pour effacer.

Cet exemple présente déjà presque tous les éléments conceptuels d'une application plus complèxe basée sur Niggle. Si vous atteignez un certain niveau de confort avec tous ces éléments, vous aurez supéré les principaux obstacles dans l'apprentissage de Niggle. Bien sûr qu'il y a pas mal d'autres prestations, et d'autres apparaîtront avec le temps, vu que Niggle, c'est un projet vivant qui se développe encore. Tout de même, les choses que vous apprendrez à partir de ce moment sont de nature plutôt incrémentale.

Annèxe: Comment utiliser MySQL ou une autre BD au lieu de fichiers plats

Remarquez que le fichier datasources2.xml contient une configuration alternative qui utilise une BD externe pour la persistance. Pour faire fonctionner cela, vous serez obligé sans doute de changer certains les paramètres JDBC_URL et JDBC_DRIVER_CLASS.

Je ne vois aucun raison pour que ça ne marche pas avec une BD quelleconque, tout en supposant qu'elle dispose d'un driver JDBC. Néanmoins, ça m'intereserrait beaucoup obtenir quelque confirmation de ça. Bien evidemment! Si vous avez fait fonctionner cet exemple avec une autre BD, ou bien si vous l'avez essayé mais il y a eu des problèmes, s'il vous plaît, écrivez-moi pour conter votre expérience!

En tout cas, une bonne approche pour le moment sera de faire que votre logique d'application fonctionne avec les fichiers plats, vu que cela n'exige pas la configuration d'aucun outil externe. Ensuite, vous pourrez changer è une BD externe au moment où il sera nécessaire.