A Generic Pagination with Lazy Datamodel in Primefaces-JSF Datatable

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 PageCriteriaobject 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 findAllmethod in the PersonService class uses the information in the PageCriteriainstance 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=trueattribute.

<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 CustomLazyDataModelto make any query for any Object with more filter, without requiring to have many implementations of LazyDataModel for each different query that we need.

4 comments

  1. 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

    1. 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.

  2. 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


    1. 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.

Leave a Reply to agamboa Cancel reply

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