Annotations Seam : Datamodel, Factory et Unwrap

Mikael Robert

L'outil Seam-Gen présenté au début de la documentation de Seam permet de générer des CRUD autour de données managées et représentées par des EJB entités très rapidement.
Les JBoss Tools (plugin eclipse) permettent d'en faire autant.
Les objets générés pour cela sont des objets héritant de EntityQuery (composant Seam) et présentant de nombreux avantages pour un CRUD simple.
Il n'y a quasiment rien à faire, et vous pouvez ajouter/mettre à jour/supprimer vos entités, et afficher les listes de données : les objets et jsf correspondants sont générés par les outils.

Seulement ces EntityQuery présentent un certain nombre d'inconvénients. Non seulement pour la personnalisation (surcharge des méthodes getRestriction, setEjbQl, etc.), mais aussi d'un point de vue propreté du code.
Le code généré par Seam est très fiable dans un contexte de CRUD simple.
Néanmoins, au sein d'une application importante, les problèmes vont vite arriver. Il est en effet difficile de modifier le scope de ces listes, ou d'ajouter des traitements aux listes en question dans l'objet sans problèmes de scope/portée des données.
Bref le but de cet article est de montrer une façon relativement rapide de développer vous-même votre liste d'entités avec Seam.
Il reprend plus ou moins un des premiers exemples de la doc officielle anglaise, mais a l'avantage d'être dans la langue de Molière pour ceux qui n'aiment pas lire l'anglais.

- Titre : Annotations Seam : Datamodel, Factory et Unwrap

- Auteur : Mikael Robert

- Parution : 27 mars 2009

- Mise à jour : 17 février 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2009 Mikael Robert. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Création de l'entité

- II. Factory, DataModel, DataModelSelection : la liste de données

- III. Côté vue : page JSF

- IV. Explications

- V. Unwrap

- VI. Conclusion

I - Création de l'entité

Nous allons donc prendre ici une Entité simple représentant une personne : son prénom, son nom, son adresse et son e-mail
Le code a été volontairement réduit pour l'article.

L'Entité gérée

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;

import org.hibernate.validator.Length;
import org.hibernate.validator.NotNull;
import org.jboss.seam.annotations.Name;

@Entity // on précise que c'est une entité
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "email")) // on ajoute une contrainte d'unicité
@NamedQueries({ // on créer des requêtes nommées (plus rapides, évitent la redondance de code)
@NamedQuery(
name = "selectPersonsByEmail",
query = "select person from Person person where person.email = "
+      ":email order by person.email"),
@NamedQuery(
name = "selectPersons",
query = "select person from Person person")
})
public class Person implements Serializable {
/** id */
@Id @GeneratedValue
private Long id; // id de l'entité, géré par Hibernate.

/** person firstName */
@NotNull
@Length(max = 50)
private String firstName;

/** person lastName */
@Length(max = 50)
private String lastName;

/* person address */
@Length(max = 50)
private String adress;

/** peson email. */
@Length(max = 50)//Longueur maximale 50 caracteres.
//utilisé pour la validation dans la JSF grâce a s:validate
@NotNull(message = "Vous devez entrer un email.") // ici on interdit que le champ soit null
@NotEmpty(message = "Vous devez entrer un email.") // ici on interdit qu'il soit vide.
@Email(message = "e-mail invalide")
private String email;

/// ....Getters/Setters And HashCode And Equals methods...
/// ....
}

Un attribut transient est ajouté à l'entité, c'est juste une petite astuce pour ne pas avoir à trop se compliquer la vie.
C'est utile si on veut par exemple faire une liste d'objets supprimables, vous allez comprendre pourquoi plus tard.

II - Factory, DataModel, DataModelSelection : la liste de données

Maintenant, pour gérer une liste de données quoi de mieux qu'un EJB Stateful ?
Bien entendu vous n'êtes pas obligé d'utiliser un EJB, vous pouvez vous contenter d'un Composant Seam simple.
Un composant Seam est moins lourd en termes de mémoire et de performances.
On commence par l'interface, locale dans le cas présent.

Interface de la liste

@Local
public interface SeamList {
/**
*  factory for the personList.
*/
void findPersons();

/**
*  select the value.
* */
void select();

/**
* delete a client.
*/
void delete();

/**
* remove method.
*/
void destroy();
}

Et on peut commencer les choses intéressantes : le corps de l'EJB en lui-même.

Implémentation de l'interface

@Stateful //
@Scope(ScopeType.CONVERSATION)
@Name("seamList")
public class SeamListBean  implements SeamList {

/** the logger. */
@Logger
private Log logger;

/** the client list. */
@DataModel
private List<Person> personList;

/** the selected value. */
@DataModelSelection
@Out(required = false)
private Person person;

/**
* the entity Manager. On récupère l'entity manager.
*/
@In(required = true)
private EntityManager entityManager;

/**
*  factory for the personList.
*/
@SuppressWarnings("unchecked")
@Factory("personList")
public void findPersons() {
try {
Query query = null;
query = entityManager.createNamedQuery("selectPersons");
this.personList = (List<Person>) query.getResultList();
} catch (NoResultException ex) {
FacesMessages.instance().addToControl("person", "Aucune personne trouvee en base.");
logger.debug("Empty person list");
}
}

/**
*  select the value.
* */
public void select() {
person.setToDelete(true);
}

/**
* delete a client.
*/
public void delete() {
personList.remove(person);
entityManager.remove(person);
person = null;
}

/**
* remove method.
*/
@Destroy
@Remove
public void destroy() {
//nothing necessary here
}

}

Quelques explications :
- @Stateful précise que c'est un EJB Stateful (Annotation JEE) ;
- @Scope(ScopeType.CONVERSATION) ici préciser le scope conversation est un peu inutile c'est plus une histoire de clarté, Seam stocke par défaut les ejb stateful en conversation. (Annotation Seam) ;
- @Name(« seamList ») déclare l'ejb comme composant Seam pour l'appeler facilement via JSF (Annotation Seam) ;
- @Logger : l'annotation Seam @Logger permet à Seam de choisir le logger du serveur d'application, sur jboss : Log4j ;
- @DataModel : ici on annote la liste @DataModel (Annotation Seam) c'est elle qui va être notre modèle de données. L'annotation en question renvoie à jsf un attribut de type liste à la page jsf sous la forme d'une instance de javax.faces.model.DataModel. Ce qui permet d'utiliser la liste dans une jsf avec une h:dataTable (ou rich:) avec des liens cliquables sur chaque ligne ;
- @DataModelSelection : et là, l'annotation Seam magique, qui va permettre de lier la ligne sélectionnée sur la page web à un objet java. L'annotation @DataModelSelection dit à seam d'injecter l'élément de liste qui correspond à la ligne cliquée ;
- @Out(required = false) : en plus d'être annoté @DataModelSelection, ici on outjecte l'élément pour l'exposer à la valeur sélectionnée directement dans la page.
Donc chaque fois qu'une ligne de la liste cliquable est sélectionnée la personne est injectée dans l'attribut du bean stateful et outjectee dans la variable nommée (contexte événement) personne.
Ce mécanisme est possible grâce au mécanisme de gestion des injections bijectif de Seam ;
- @In(required = true) : on injecte l'entityManager en précisant qu'il est obligatoire. (Annotation Seam) ;
- @Factory(« personList ») : encore une annotation magique de Seam : le @Factory explique à Seam de créer une instance de SeamListBean puis d'invoquer la méthode findPersons pour initialiser l'objet.
C'est la méthode de construction de personnes. Certains y verront un lien avec un design pattern bien connu.
Le @SupressWarnings est uniquement là du fait qu'eclipse lève un Warning du fait du cast obligatoire en List<Person> ;
- @Destroy : dans Seam tous les composants stateful doivent avoir une méthode sans paramètres annotés ainsi.
Il l'utilise ainsi pour retirer les composants Stateful lorsque les contextes se terminent et nettoyer la mémoire.

Vous pouvez donc voir grâce à la méthode delete que grâce au DataModelSelection couplé au DataModel, qu'on peut supprimer très simplement l'élément de la ligne courante.

III - Côté vue : page JSF

Et enfin la page JSF pour afficher tout ça :
on utilise une rich:dataTable pour lister les données, un rich:dataScroller pour la pagination

La page JSF de visualisation de la liste

<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:s="http://jboss.com/products/seam/taglib"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:rich="http://richfaces.org/rich"
template="/layout/template.xhtml">

<ui:define name="body">
<rich:panel id="personlist" rendered="#{not empty personList}">
<f:facet name="header">Donnees en base</f:facet>

<h:form>
<rich:spacer height="15"/>
<rich:datascroller for="personList"
boundaryControls="auto" stepControls="hide" fastControls="hide" renderIfSinglePage="false"/>

<rich:spacer height="30" />

<rich:dataTable id="personList" var="person" value="#{personList}" style="text-align: center;" rows="30">
<rich:column>
<f:facet name="header">Email</f:facet>
<h:commandLink id="personmail" value="#{person.email}"  action="#{seamList.select}" />
</rich:column>
<rich:column>
<f:facet name="header">Nom</f:facet>
<h:outputText value="#{person.lastName}" />
</rich:column>
<rich:column>
<f:facet name="header">Prenom</f:facet>
<h:outputText value="#{person.firstName}" />
</rich:column>
<rich:column>
<f:facet name="header">Action</f:facet>
<h:commandLink action="#{seamList.delete}"  alt="Supprimer" >
<h:graphicImage value="/img/delete.png" />
</h:commandLink>
</rich:column>
</rich:dataTable>

</h:form>
</rich:panel>
</ui:define>

</ui:composition>

IV - Explications

Ici on a vu donc une liste avec état conservé pour effectuer des actions dessus.
Je l'ai placée en conversation par choix personnel (et les EJB Statefuls sont placés par défaut en conversation par Seam), mais si vraiment votre liste est énorme, vous pouvez la placer en session pour éviter les chargements multiples

Attention cependant aux problèmes de scope courants avec la session… Personnellement je mets le moins d'objets possible en Session pour qu'ils aient un cycle de vie le plus court possible.

Autre conseil, si vous voulez que votre rich:dataTable soit paginée, car vous avez beaucoup de données : vous pouvez utiliser un rich:dataScroller mappé à votre dataTable pour le faire simplement.
Gros inconvénient : la liste entière est chargée au premier chargement de la page, et pas uniquement les données affichées.

Je vous conseille donc de vous développer un système de pagination avec lazy loading assez générique pour vos listes de tailles importantes en utilisant les facilités de l'EjbQL : c'est très simple (setMaxResult, setFirstResult…).
Je rédigerai prochainement un article sur la pagination en lazy loading en utilisant le rich:dataScroller.

V - Unwrap

Maintenant si vous voulez une liste systématiquement rechargée (Stateless), il existe une autre façon de faire :

Utilisation d'Unwrap

@Name("personList")
@Scope(ScopeType.STATELESS)
@AutoCreate
public class PersonList {

@In EntityManager entityManager;

@Unwrap
public List<Person> getPersonList() {
return (List<Person>) entityManager.createQuery("selectPersons")
.setHint("org.hibernate.cacheable", true)
.getResultList();
}
}

Ce composant utilise un contexte de persistance géré par Seam.
Contrairement à l'exemple précédent, c'est Seam qui gère le contexte de persistance et pas le conteneur d'ejb.
La liste va ici être rechargée à chaque fois que la page le sera.
L'annotation Unwrap dit à Seam de fournir en type de retour celui de la méthode getPersonList : List<Person>, au lieu du composant PersonList quand on appellera l'objet via #{personList} dans une JSF.

VI - Conclusion

Voilà, vous savez maintenant construire rapidement des listes simples à gérer et plutôt performantes.
On a vu à quel point l'annotation @DataModelSelection est utile,
elle permet de directement mapper la ligne sélectionnée dans la page, à un objet de votre liste.