Dreamisle.net Blog

Java, Rails, Security and so many ...

Système De Pagination en Lazy Loading Avec Seam, JPA Et Richfaces.

| Comments

Lorsqu’on gère de gros volumes de données, il devient vite essentiel de monter un système de pagination digne de ce nom. D’une part pour faciliter la visualisation par l’utilisateur, et d’une autre pour éviter l’explosion de la mémoire au chargement de vos données.

Nous allons ici apprendre à mettre en place un système de pagination qui va automatiquement gérer un cache mémoire correspondant uniquement aux données de la page courante visualisée.

Nous allons aussi, plutôt que de réinventer la roue utiliser le data:scroller de richfaces, pour gérer la navigation entre les pages.

Vous allez le voir ce système est basé sur la simplicité, j’ai toujours pensé que les systèmes les plus simples étaient les plus efficaces.

Dans le cas présent nous paginerons une liste de Text Entry (une entité simple pour persister du contenu utilisateur dans un de mes projets), mais ce système est applicable à n’importe quel type d’entité correctement écrite.

J’ai souvent vu des systèmes de pagination, mais dans beaucoup de cas, on ne fait que gérer l’affichage page par page côté utilisateur avec un simple dataScroller, et aucun code derrière pour gérer la persistance des données en mémoire

JSF

Commençons par voir la JSF qui affiche notre liste : Vous le voyez ici c’est très simple; on a une dataTable pour afficher la liste des données, et on y a adjoint un rich:dataScroller pour paginer. La subtilité dans cette JSF reside seulement dans la mise en place de la variable page pour le datascroller.

<h:form>
<rich:datascroller for="textEntryListTable" page="#{textEntryList.currentPage}"/>
<rich:dataTable id="textEntryListTable" value="#{textEntries}"  var="entry" style="border-style: none;"  rows="5">
    <rich:column style="border-bottom: 0px; border-right: 0px;">
     <rich:panel>
      <f:facet name="header"><h:outputText value="#{entry.getFormatedTitle()}"  /></f:facet>
      <h:outputText value="#{entry.content}" escape="false" />
     </rich:panel>
     <rich:spacer height="25"/>
     </rich:column>
  </rich:dataTable>
</h:form>

Lazy List

On a donc besoin d’un composant qui permet de gérer un cache mémoire, de taille variable (le nombre d’élément voulu par page) et qui ne charge QUE les données de la page courante. Cet objet devra donc travailler conjointement avec une liste classique pour faire son travail de manière quasi transparente. Voyons un peu le code de cet objet.

/**
 * @author mikael.robert
 * This list load data with lazy method to avoid problem of memory for big data list.
 * @param <T> the object type.
 */
public class LazyList<T> extends AbstractList<T> {

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

    /**
     * The query.
     *
     * */
    private Query query;

    /**
     * cache for loaded datas.
     * */
    private Map<Integer, T> loaded;

    /**
     *  total number of results.
     *  */
    private long numResults;

    /**
     * currentPage.
     */
    private int currentPage;


    /**
     * the page size.
     * */
    private int pageSize;

    /**
     * constructor.
     * */
    public LazyList() {
        loaded = new HashMap<Integer, T>();
    }

    /**
     * Create a LazyList backed by the given query, using pageSize results
     * per page, and expecting numResults from the query.
     * @param query the query to get the datas.
     * @param pageSize the page size wanted.
     * @param numResults the number of results of the dataset.
     * @param currentPage the currentPage.
     */
    public LazyList(Query query, int pageSize, long numResults, int currentPage) {
        this();
        this.query = query;
        this.pageSize = pageSize;
        this.numResults = numResults;
        this.currentPage = currentPage;
    }

    /**
     * Fetch an item, loading it from the query results if it hasn't already
     * been.
     * @param i the first result.
     * @return object the object.
     */
    @SuppressWarnings("unchecked")
    public T get(int i) {
          if (!loaded.containsKey(i)) {
            List<T> results = (List<T>) query.setFirstResult(i).setMaxResults(pageSize).getResultList();
            for (int j = 0; j < results.size(); j++) {
                loaded.put(i + j, results.get(j));
            }
        }
        return loaded.get(i);
    }

    /**
     * Return the total number of items in the list. This is done by
     * using an equivalent COUNT query for the backed query.
     * @return size the size.
     */
    public int size() {
        return (int) numResults;
    }

     /**
      * update the number of results expected in this list.
      * @param numResults the number of results total.
      * */
    public void setNumResults(long numResults) {
        this.numResults = numResults;
    }

    /**
     * get the loaded cache size.
     * @return size the size.
     */
    public int getLoadedSize() {
        return loaded.size();
    }
}

Comme vous pouvez le voir cet objet dérive de AbstractList : au lieu d’utiliser un ArrayList ou autre, on utiliser celui-ci, mais de la même manière qu’une autre list. Ainsi le développement est transparent. Cet objet possède quelques attributs :

  • Query query; La requète de récupération des données passée à l’objet lors de la construction.

  • Map<Integer, T> loaded; Le cache mémoire.

  • long numResults; Le nombre total de données présentes en base.

  • int currentPage; La page courante.

  • private int pageSize; La taille d’une page.

Tout d’abord certains puristes dirons que ce n’est pas génial de conserver la requête JPA dans l’objet. Personnellement je pense que ce n’est pas un problème car il sera accédé par un objet interface entre lui et le client ce qui limite fortement les risques. C’est le code de la méthode surchargée get qui est important ici. Il est relativement simple, mais c’est lui qui va permettre de ne charger que ce qui est nécessaire, et permettre l’utilisation du cache pour ne pas recharger ce qui l’est déjà.

Grosso modo, lorsque le composant rich:dataTable utilisé dans la partie JSF de ce billet, va faire appel au get d’un élément, si celui-ci n’est pas déjà chargé alors on le récupère. Cette récupération est faite un utilisant la requête a la quelle on fixe comme premier résultat, l’indice courant. On fixe à la requête un nombre maximum de résultat correspondant à la taille de page. Ainsi on ne récupère que les éléments de la page courante. Cependant, il y a un seul petit hic à ce fonctionnement du à priori au rich:dataScroller. Je n’ai pas vraiment compris pourquoi, mais j’ai constaté, que j’avais toujours deux pages chargées dans mon cache. Je suppose fortement que c’est le composant rich:dataScroller qui joint à une dataTable, précharge deux pages de données pour accélérer la navigation. En effet j’ai refait un dataScroller pour des tests et là je n’avais qu’une page en mémoire. Si quelqu’un a plus d’infos la dessus je suis interessé ! Néanmoins ce n’est pas bien génant d’avoir deux pages en cache.

Enfin, ceux qui ont bien compris le fonctionnement vont se dire qu’on accumule tout ce qu’on charge au fur et à mesure dans le cache. En fait c’est dans l’utilisation de la lazy list qu’on va gérer ça, je vous propose de passer à la partie suivante pour mieux comprendre.

Gestion de la liste

Vous allez donc pouvoir utiliser votre lazy list. Dans un précédent article j’avais expliqué le fonctionnement de la Factory, de DataModel et de DataModelSelection. Nous allons ici les réutiliser, mais conjointement à un autre composant que nous verrons un peu après, qui sera lui chargé de gérer la mémoire.

On a donc ici une gestion de liste quasiment identique à qu’on peut faire d’habitude avec Seam. Comme d’habitude j’ai enlevé les getters/setters pour ne pas surcharger le code, mais ils sont nécessaires. Jetons donc un oeil à ce code :

/**
* dreamisle-cms
* Copyright (C) 2009 Mikael Robert
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

package org.dreamisle.services.blog;

import java.util.List;

import javax.ejb.Remove;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;

import org.dreamisle.entities.Comment;
import org.dreamisle.entities.TextEntry;
import org.dreamisle.services.utils.LazyList;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Begin;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Destroy;
import org.jboss.seam.annotations.End;
import org.jboss.seam.annotations.Factory;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Out;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.datamodel.DataModel;
import org.jboss.seam.annotations.datamodel.DataModelSelection;
import org.jboss.seam.faces.FacesMessages;
import org.jboss.seam.international.StatusMessage.Severity;
import org.jboss.seam.log.Log;

/**
 * text Entry list. Seam Query Components.
 * @author leakim
 *
 */
@Name("textEntryList")
@Scope(ScopeType.CONVERSATION)
public class TextEntryList {

    /**
     * The page Size.
     */
    public static final int PAGE_SIZE = 5;

    /**
     * The currentPage.
     */
    private int currentPage  = 1;

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

    /**
     * Entity manager.
     */
    @In
    private EntityManager entityManager;

    /**
     * The result list.
     */
    @DataModel
    private List<TextEntry> textEntries;

    /**
     * The current text entry.
     */
    @Out(required = false)
    @DataModelSelection
    private TextEntry textEntry;

    /**
     * Init method.
     */
    @Create
    @Begin
    public void init() {
        logger.debug("Create Text entry list manager.");
    }

    /**
     * return the result list.
     *
     *
     */
    @Factory("textEntries")
    public void initTextEntries() {
        logger.debug("REINITIALIZE LIST");
        String ejbQl =  "from TextEntry textEntry order by textEntry.date asc";
        StringBuilder ejbQLStd = new StringBuilder("select textEntry ");
        StringBuilder ejbQLCount = new StringBuilder("select count(textEntry) ");
        ejbQLCount.append(ejbQl);
        ejbQLStd.append(ejbQl);
        try {
            Query query = entityManager.createQuery(ejbQLStd.toString());
            long count = (Long) entityManager.createQuery(ejbQLCount.toString()).getSingleResult();
            textEntries = new LazyList<TextEntry>(query, PAGE_SIZE, count, currentPage);
        } catch (NoResultException ex) {
            logger.error("No results found in text entry");
            FacesMessages.instance().add(Severity.WARN, "No results found in text entry");
        }
    }

    /**
     * Destroy method.
     */
    @End
    @Destroy
    public void destroy() {

    }
}

Comme vous avez pu le voir : - Cet objet est maintenu en conversation, afin de permettre de conserver l’état de l’objet ( ici la page courante et le cache mémoire). - J’ai mis en place une méthode annotée @Create, pour pouvoir forcer le début de la conversation lors de la création de l’objet. Idem pour le @Destroy + @End, ainsi on est sur et certain que la conversation commence et termine quand on le souhaite, sans perte de l’état. - Nous avons une constante qui correspond à la taille de la page, et une variable currentPage qui correspond à la page courante visualisée par l’utilisateur. - La lazy list est reconstruite à chaque utilisation de la Factory : à chaque changement de page. Pour initializer le nombre maximum de résultats (le numResults de la LazyList) on réalise la mère requète mais avec un count, beaucoup plus rapide à éxécuter qu’une recupération de données. On passe à la Lazy List la taille de la page, et la page courante. Ici on n’utilise pas la page courante, mais je l’ai laissée car j’ai un projet ou a la lazylist a des méthodes utilaitres qui en ont besoins. A vous d’améliorer la Lazylist selon votre besoin.

Comme vous le voyez ce système n’est pas très compliqué, mais est efficace. Ce n’est probablement pas le plus efficace ni la meilleure façon de faire, mais c’est très rapide à mettre en place, et très simple à réutiliser.

Voilà j’espère que ce billet vous aura été utile.