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

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.

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.

Leave a Reply

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