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 PageCriteria
y 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 LazyDataModel
que 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 findAll
en 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 CustomLazyDataModel
para 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.
Hola,
Excelente explicacion, solo tengo una duda, como es la clase IModel de la cual extiende el Tipo de dato
saludos
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
Necesitan identificar el Id, para identificar cada registro. Luego en cada clase implemento según la necesidad.
public interface IModel {
Long geId();
}
Sorry for the inconvenience, but what would the PersonService code be?
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
String sql = "select l from Person l where ...";
String countSQL = "select count(l.id) from where ...";
List
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/
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
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.
}
puedes compartir el código completo?
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)
El secreto en esta implementación “lazy” está en utilizar una expresión Lambda, incluidas en Java 8.
CustomLazyDataModel
, recibe como parámetro unDataPageable
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:
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étodoload
donde se llama adataPageable.getData(pageCriteria)
, ¿Qué ejecuta eso? pues la implementación deDataPageable
proporcionada como una expresión lambda.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étodoload
donde se llama adataPageable.getData(pageCriteria)
, una lineas antes se crea el objeto pageCriteria que será enviado a la expresión lambda.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.