When we have to display a large records in a data-table using pagination is the best approach to optimize memory and response time, so the user experience is not impacted. Instead of loading the complete set of records, we just want to load those that are displayed. If we change the page (next, previous, last), or apply a filter, or sort a column, we will request a new page with a new sub-set of data.
Eventhough, java script frameworks like React JS, Angular or VueJS are usually used to develop the front-end of the application, Java Server Faces has still a niche for some kind of application, mainly Java Enterprise based on big forms. And in Java Server Faces, Primefaces is definitely the king regarding UI components.
In this article, I would like to share the approach I use to simplify how to handle pagination with lazy data model in JSF (primefaces). Hopefully it can be useful for other people.
Let´s start by defining a couple of classes:
- PageCriteria: Contains the criterias to know which subset of data we want to load
- PageResult: Contains the subset of data found
- PageableData: Functional interface to make magic.
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 + '}'; } }
As we can see PageResult
is using generics, then we can using to return any Type we need. Users, Person, Car, whatever type.
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; } }
We create a Functional interface with one method (the name doesn’t matter), but what it matters is the received parameter and the return object. As we can see it will receive a PageCriteria
object and return a PageResult
object. We will use this interface to use Java Lambdas in a next step.
@FunctionalInterface public interface DataPageable { PageResult getData(PageCriteria pageCriteria); }
As we mentioned we are using Primefaces, and it already provides a LazyDataModel
class which we should extend. When we declare a datatable to be lazy=true
internally every time the data table requires to display a new set of data (first load, next page, previous page, filtering or sorting), then the method load(int first, int size, String sortColumn, SortOrder sortOrder, Map filterBy)
will be called.
However, we can to provide our own CustomLazyDataModel
implementation which is able to handle in a generic way any query for any data-type. That’s why the constructor of our CustomLazyDataModel
receives an instance of DataPageable
which is a Type for a Lambda expression.
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; } }
In the load(...)
method we will call the method getData(...)
of the DataPageable
instance, in other words, we are executing the lambda expression.
@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; } }
So, when we declare the CustomLazyDataModel
in our Controller
we indicate a lambda expression with the call to the service method in charge of getting the subset of data. Here is were the magic happens!
We can use the same CustomLazyDataModel to call whatever method we want to load the subset of data. The only requirement is that metho should return a PageResult
type.
In this example the findAll
method in the PersonService
class uses the information in the PageCriteria
instance to load that information from the the data base, and the return the PageResult
object, but we can call any other endpoint: a RestAPI, a WebService, a GraphQL endpoint, to get that data.
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()); }
Finally, this is how we declare the datatable in the HTML of JSF. Note the lazy=true
attribute.
<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>
Conclusion
With this simple approach we can use the CustomLazyDataModel
to make any query for any Object with more filter, without requiring to have many implementations of LazyDataModel
for each different query that we need.
Hi Gamboa,
Thanks for the valuable post!
I am tying to implement this but getting few errors.
could you please provide the details of the following file ?
com.orthanc.myapp.model.IModel
Thanks,
Poorna
public interface IModel {
Long geId();
}
Why does I use it?
Because that why I can identify the objects by Id in the `CustomLazyDataModel` methods.
– T getRowData(String rowKey)
– Object getRowKey(T object)
But, it’s optional.
sorry im new in java, could you please share with me what is content of this class
“import com.orthanc.myapp.model.IModel;”
thanks in advance
public interface IModel {
Long geId();
}
Why does I use it?
Because that why I can identify the objects by Id in the `CustomLazyDataModel` methods.
– T getRowData(String rowKey)
– Object getRowKey(T object)
But, it’s optional.
Great, can you also update it for Primefaces 11?