Paginación con un Lazy Datamodel en Primefaces-JSF Datatable

Advertisements

Cuando necesitamos mostrar un gran numero de registros en un data-table el uso de paginación es la forma adecuada para optimar memoria y tiempo de respuesta, de esta manera la experiencia del usuario no es impactada. En lugar de cargar todos los registros, solamente queremos cargar aquellos que son desplegados al usuario. Si el usuario cambia de página (siguiente pagina, anterior, ultima), o aplica un filtro o ordena por un columna, vamos a hacer un nueva solicitud para cargar la información de la nueva página con el respectivo subset de datos.

A pesar que frameworks java-script como React JS, Angular o VueJS, actualmente son los más usados para el desarrollo de Front-end de las aplicacionesare, Java Server Faces aún tiene un nicho para algunos tipos de aplicaciones, principalmente Java Enterprise basadas en grandes formularios con complejas interacciones. En Java Server Faces, Primefaces es en definitiva el rey cuando se refiere a componentes UI.

En este artículo, me gustaría compartir la forma en como suelo simplificar el como manejar la paginación como lazy data model en JSF (Primefaces). Espero, le sea util a alguin más.

Vamos a comenzar definiendo unas cuantas clases:

  • PageCriteria: Contiene los criterios para conocer cual es el subset the datos que queremos cargar.
  • PageResult: Contiene el subset de datos encontrados
  • PageableData: Interface Functional, clave para hacer la magia.
import java.io.Serializable;
import java.util.Map;

/**
 *
 * @author Adam M. Gamboa G
 */
public class PageCriteria implements Serializable {
    
    private int first;
    private int size;
    private Map<String, Object> filteredBy;
    private Boolean isAscending;
    private String sortedBy;
    
    public int getFirst() {
        return first;
    }

    public void setFirst(int first) {
        this.first = first;
    }

    public int getSize() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    public Map<String, Object> getFilteredBy() {
        return filteredBy;
    }

    public void setFilteredBy(Map<String, Object> filteredBy) {
        this.filteredBy = filteredBy;
    }

    public Boolean getIsAscending() {
        return isAscending;
    }

    public void setIsAscending(Boolean isAscending) {
        this.isAscending = isAscending;
    }

    public String getSortedBy() {
        return sortedBy;
    }

    public void setSortedBy(String sortedBy) {
        this.sortedBy = sortedBy;
    }

    @Override
    public String toString() {
        return "PageCriteria{" + "first=" + first + ", size=" + size + ", filteredBy=" + filteredBy + ", isAscending=" + isAscending + ", sortedBy=" + sortedBy + '}';
    }   
}

Como podemos ver PageResult está usando genericos, por lo tanto podemos usarlo para retornar cualquier tipo de dato que necesitemos: Users, Person, Car, cualquier tipo.

import java.io.Serializable;
import java.util.List;

/**
 *
 * @author Adam M. Gamboa G
 * @param <T>
 */
public class PageResult<T> implements Serializable{
    private final List<T> data;
    private final int size;
    private final int index;
    private final long count;

    public PageResult(List<T> data, long count, int index, int size) {
        this.data = data;
        this.size = size;
        this.index = index;
        this.count = count;
    }
    
    public List<T> getData() {
        return data;
    }

    public int getSize() {
        return size;
    }

    public int getIndex() {
        return index;
    }

    public long getCount() {
        return count;
    }  
}

Creamos una interface funcional, la cual tiene por lo tanto solamente un método (el nombre no interesa), pero lo que realmente intersa es el parámetro que recibe y el objeto que retorna. Como podemos ver, recibe un objeto PageCriteriay returna un objeto PageResult. Vamos a usar esta interface en los siguientos para usar una expresión Lambda de Java.

@FunctionalInterface
public interface DataPageable {
    PageResult getData(PageCriteria pageCriteria);
}

Tal como lo indicamos al inicio, estamos usando Primerfaces, y este ya proporciona un clase LazyDataModelque debemos de extender. Cuando se declara un datatable con lazy=true, internamente cada vez que el datatable requiere mostrar nuevos datos (primera carga, cambiar de pagina, filter, ordenar una columna), el método load(int first, int size, String sortColumn, SortOrder sortOrder, Map filterBy)será llamado.

Advertisements

Sin embargo, nosotros podemos proveer nuestra propia implementación CustomLazyDataModel que sea capaz de manejar de forma generica cualquier consulta para cualquier tipo de dato. Es por esta razón que el constructor de CustomLazyDataModel recibe una instancia de DataPageable, la cual sera una expresión Lambda.

import com.orthanc.myapp.web.common.util.JSFUtil;
import com.orthanc.myapp.model.IModel;
import com.orthanc.myapp.model.pagination.DataPaginable;
import com.orthanc.myapp.model.pagination.PageCriteria;
import com.orthanc.myapp.model.pagination.PageResult;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.primefaces.model.LazyDataModel;
import org.primefaces.model.SortOrder;

/**
 * @author Adam M. Gamboa González
 * @param <T>
 */
public class CustomLazyDataModel<T extends IModel> extends LazyDataModel<T>{

    private PageResult<T> page;
    private DataPageable dataPageable;
    
    public CustomLazyDataModel(DataPageable dataPageable){
        this.dataPageable = dataPageable;
    }

    @Override
    public List<T> load(int first, int size, String sortColumn, SortOrder sortOrder, Map<String, Object> filterBy) {
        try {
            PageCriteria pageCriteria = new PageCriteria();
            pageCriteria.setFilteredBy(filterBy);
            pageCriteria.setFirst(first);
            pageCriteria.setSize(size);
            pageCriteria.setSortedBy(sortColumn);
            pageCriteria.setIsAscending(sortOrder == null ? null : 
                    (sortOrder == SortOrder.ASCENDING ? 
                            true : (sortOrder == SortOrder.DESCENDING ? false : null)));
            page = this.dataPageable.getData(pageCriteria);
        } catch (Exception ex) {
            JSFUtil.addErrorMessage(ex.getMessage());
            page = new PageResult<>(new ArrayList<>(), 0, first, size);
        }
        this.setRowCount((int)page.getCount());
        return page.getData();
    }
    
    @Override
    public void setRowIndex(int rowIndex) {
        if (rowIndex == -1 || getPageSize() == 0) {
            super.setRowIndex(-1);
        } else {
            super.setRowIndex(rowIndex % getPageSize());
        }
    }
    
    public T getRowData(String rowKey) {
        List<T> data = (List<T>) getWrappedData();
        for (T d : data) {
            if(d.getId()!= null){
                if(d.getId().toString().equals(rowKey)){
                    return d;
                }
            }
        }
        return null;
    }

    public Object getRowKey(T object) {
        return object != null ? object.getId() : null;
    }
}

En el método load(...)vamos a llamar el método getData(...)definido en la instancia de interface funcionalDataPageable , en otras palabras, estamos invocando la expresión Lambda.

@ViewScoped
@Named
public class MyController{
 	private CustomLazyDataModel dataModel;
  	
    @Inject
    private PersonService PersonService;
  
  	public initializeDataTable(){
       dataModel = new CustomLazyDataModel<Person>(pc -> myService.findAll(pc));
    }
  
    public CustomLazyDataModel getDataModel(){
        return dataModel;
    }
}

Entonces, cuando declaramos la variable CustomLazyDataModel en nuestroController indicamos una expresión Lambda con la llamada al servicio a cargo de consultar y obtener el subset de datos a mostrar. Aquí es donde la magia sucede!

Podemos usar el mismo CustomLazyDataModel para llamar cualquier método que queremos que cargue los datos. El único requiremiento es que el método debe de retorna un tipo PageResult.

En este ejemplo, el método findAllen la clase PersonService usa la informacion de la instancia PageCriteria para cargar la información paginada desde la base de datos, y retornar el objeto PageResult. Pero podemos hacer llamada a endpoints diferentes, un API rest, un WebService, GraphQL para obtener la información.

 public PageResult<Person> findAll(PageCriteria criteria) {
        String sql = "select l from Person l where ...";
        String countSQL = "select count(l.id) from where ...";
        
        List<Person> data = //get list result of the page
        long count = //get total count
        return new PageResult<>(data, count, criteria.getFirst(), criteria.getSize());
    }

Finalmente, esto es como declaro el datatable en el HTML de JSF. Noten el atributo lazy=true.

<p:dataTable id="myDataTable"
                 widgetVar="myDataTable"
                 var="person" 
                 value="#{myController.dataModel}"
                 rows="20"
                 lazy="true"
                 paginator="true" 
                 paginatorPosition="bottom"  
                 filterEvent="enter"
                 >
        <p:column headerText="#{label['name']}" 
                  filterBy="#{person.name}">
            <h:outputText value="#{person.name}"/>
        </p:column>

        <p:column headerText="#{label['lastname']}" 
                  filterBy="#{person.lastname}">
            <h:outputText value="#{loan.lastname}"/>
        </p:column>
    </p:dataTable>

Conclusión

Con esta sencilla forma podemos usar el CustomLazyDataModelpara hacer consultas paginadas a cualquier objeto, endpoint de mandera muy simple, podemos tener incluso más párametros en el servicio que llamamos desde la expresión lambda, sin necesitar tener muchas implementaciones diferentes de LazyDataModel para cada diferente query que necesitemos.

Advertisements

10 comments

  1. Hola,
    Excelente explicacion, solo tengo una duda, como es la clase IModel de la cual extiende el Tipo de dato

    saludos

    1. En mi caso IModel es una interface con un método abstract `Long getId();`

      Ahora la pregunta podría ser, para que la creo utilizo. La respuesta.
      En la clase `CustomLazyDataModel` los métodos

      • T getRowData(String rowKey)
      • Object getRowKey(T object)

      Necesitan identificar el Id, para identificar cada registro. Luego en cada clase implemento según la necesidad.


      public interface IModel {
      Long geId();
      }

    1. Hi Jesus, the real implementation will depend on your way to load that data, using JDBC, JPA, JPA+Criteria, or even calling a Rest API.
      But in summary, this is an idea of the structure that is required.


      public class PersonService {
      public PageResult findAll(PageCriteria criteria) {
      String sql = "select l from Person l where ...";
      String countSQL = "select count(l.id) from where ...";
      List
      data = //get list result of the page
      long count = //get total count
      return new PageResult<>(data, count, criteria.getFirst(), criteria.getSize());
      }
      }

      One example of paginated queries on database using JPA + Criteria query is this article.
      https://blog.adamgamboa.dev/using-jpa-criteriaquery-to-build-paginated-queries/

  2. When doing the implementation I get the following error in the console by chance you will know the reason
    java.lang.ClassCastException: com.prueba.prueba1.tos.PagoTO cannot be cast to com.prueba.prueba1.implementacionlazy.IModel
    java.lang.ClassCastException: com.prueba.prueba1.tos.PagoTO cannot be cast to com.prueba.prueba1.implementacionlazy.IModel

    1. Without looking at your code, it will be hard to me. But I would say that you class `PagoTO` is not implementing the interface `IModel`.

      “`
      package com.prueba.prueba1.tos;
      import com.prueba.prueba1.implementacionlazy.IModel;
      public class PagoTO implements IModel {
      // Your code here.
      }

  3. Una consulta : en el controlador, usas una instancia de PageCriteria.
    public initializeDataTable(){
    dataModel = new CustomLazyDataModel(pc -> myService.findAll(pc));
    }
    ese metodo es void imagino y la variable pc donde la creas y como?
    muchas gracias por tu ejemplo.
    Es un gran aporte.
    lo has subido a git o algo asi ?
    Maxi castiglioni (Montevideo uruguay)

    1. El secreto en esta implementación “lazy” está en utilizar una expresión Lambda, incluidas en Java 8.

      CustomLazyDataModel, recibe como parámetro un DataPageable que una Interface Funcional (Functional Interface).
      En lugar de CustomLazyDataModel recibir una instancia, puede recibir una expresión lambda, pc -> myService.findAll(pc) es dicha expresión. La expresión lambda no es ejecuta en ese momento, es algo que será ejecutado después (de ahí el comportamiento lazy).

      Para sus preguntas:

      ese método es void imagino

      la respuesta es no, no es void, myService.findAll(pc) returna un objeto de tipo PageResult.
      public PageResult findAll(PageCriteria criteria).
      Es dentro de la clase CustomLazyDataModel, el método load donde se llama a dataPageable.getData(pageCriteria), ¿Qué ejecuta eso? pues la implementación de DataPageable proporcionada como una expresión lambda.

      la variable pc donde la creas y como?

      pc, es el parámetro de la expresión lambda, si ve la definición de la interface Funcional DataPageable

      public interface DataPageable {
      PageResult getData(PageCriteria pageCriteria);
      }

      Se recibe un parámetro de tipo PageCriteria (pc), en la expresión lambda ese es el parámetro que se recibe.

      Ahora, ¿Dónde se envía es pageCriteria y con qué datos? volvemos de nuevo a la clase CustomLazyDataModel, el método load donde se llama a dataPageable.getData(pageCriteria), una lineas antes se crea el objeto pageCriteria que será enviado a la expresión lambda.

      1. Al utilizar expresiones Lambda en Java, nos introducimos un poco en el paradigma funcional, donde las cosas no se ejecutan de inmediato, si no, un método se envía como parámetro y es ejecutado luego.
        En Java lo hacemos por medio de las Interfaces Funcionales y las expresiones Lambda.

        Dado, que este enfoque debe ser génerico (para cualquier tipo de dato o estructura), el CustomLazyDataModel debe tener algún tipo de “contrato” con el servicio, es por esto que todos los servicios siempre van retornar un “PageResult” y recibir un parámetro “PageCriteria”.
        Además el servicio podría recibir otros parámetros de ser necesario, solamente se indican en su llamado dentro de la expresión Lambda.

Leave a Reply to agamboa Cancel reply

Your email address will not be published. Required fields are marked *