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ésenteés 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 et présentent 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 correspondant 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é de code. Le code généré par Seam est très fiable dans un contexte de CRUD simple. Mais 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 ce billet est de montrer une façon relativement rapide de developper 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 à l’avantage d’être dans la langue de Molière pour ceux qui n’aiment pas lire l’anglais.
Nous allons donc prendre ici une Entité simple représentant une personne : Le code a été volontairement réduit pour l’article.
@Entity // on précise que c'est une entité
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "email")) // on ajoute une contrainte d'intégrité
@NamedQueries({ // on créer des requêtes nommées (plus rapides, et é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;
/** 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)
@NotNull(message = "Vous devez entrer un email.")
@NotEmpty(message = "Vous devez entrer un email.")
@Email(message = "e-mail invalide")
private String email;
/** true if value is to delete.
*@Transient permet de ne pas stocker l'attribut en base
*/
@Transient
private boolean toDelete;
/// ....Getters/Setters And HashCode And Equals methods...
/// ....
}
Un attribut transient est ajoute à l’entité, c’est juste une petite astuce pour ne pas avoir a trop se compliquer la vie si on veut par exemple faire une liste d’objet à supprimer, vous allez comprendre pourquoi plus tard.
Maintenant, pour gérer une liste de données quoi de mieux qu’un EJB Stateful ? C’est parti … On commence par l’interface, locale dans le cas présent.
@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 interessantes : le corps de l’ejb en lui meme.
@Stateful // on precise que c'est un EJB Stateful
@Scope(ScopeType.CONVERSATION) // ici preciser le scope conversation est un peu inutile c'est plus
//une histoire de clarte, Seam stocke par defaut les ejb stateful en conversation.
@Name("seamList") // on en fait un composant Seam pour l'appeller facilement via JSF
public class SeamListBean implements SeamList {
/** the logger. */
@Logger // l'anotation Seam @Logger permet a Seam de choisir le logger du serveur d'appli
// sur jboss : Log4j
private Log logger;
/** the client list. */
@DataModel // ici on annote la liste @DataModel c'est elle qui va être notre modele de données.
// l'annotation en question renvoi a jsf un attribut de type liste a 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.
private List<Person> personList;
/** the selected value. */
@DataModelSelection
// et la l'annotation magique, qui va permettre de lier la ligne sélectionnée sur la
// page web a un objet java.
// l'annotation @DataModelSelection dit a seam d'injecter l'element de liste qui correspond a la
//ligne cliqué.
@Out(required = false)
// ici on outjecte l'element 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 evenement) personne.
private Person person;
/**
* the entity Manager. On récupère l'entity manager.
*/
@In(create = true, required = true)
private EntityManager entityManager;
/**
* factory for the personList.
*/
@SuppressWarnings("unchecked")
@Factory("personList") // Encore une anotation magique : le @Factory ici explique a Seam de creer
//une instance de SeamListBean puis d'invoquer la methode findPersons pour intialiser l'objet. C'est
//la méthode de construction de personnes. Certains y verront un lien avec un design pattern bien
//connu.
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 // dans Seam tous les composants stateful doivent avoir une methode sans parametres annotés
// ainsi. Il l'utilise ainsi pour retirer les composants Stateful lorsque les contextes se terminent et
// netoyer la memoire.
public void destroy() {
//nothing necessary here
}
}
Et enfin la jsf pour afficher tout ça :
<!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"><h:outputText value="Supprime" /></f:facet>
<h:selectBooleanCheckbox value="#{person.toDelete}"/>
</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>
Enfin pour revenir sur l’attribut en @Transient dans l’entité au début de ce billet. L’idée est qu’avec ce type de conception de listes Seam on ne peut pas sans moyen alternatif faire de suppression multiple sur la liste, Imaginez que vous souhaitez avoir une check box au bout de chaque ligne pour supprimer plusieurs éléments a la fois (pas forcement toute la liste.) Et bien ainsi vous pouvez directement les mapper a l’entité en question sans avoir besoin de faire une association entre un attribut de bean quelconque et votre entité.
Ici on a vu donc une liste avec état conservé pour effectuer des actions dessus. Je l’ai placé en conversation par choix personnel, 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 met le moins d’objet 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:paginator mappe à 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 taille importantes en utilisant les facilités de l’ejbql c’est très simple (setMaxResult, setFirstResult …).
Maintenant si vous voulez une liste systématiquement rechargée (stateless) il existe une autre façon de faire :
@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)
.getSingleResult();
}
}
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’annotations Unwrap dis a Seam de fournir en type de retour, celui de la méthode getPersonList : List