By Moritz Becker on 03 February 2017
In this post I describe how to implement a paginated data table with filtering and sorting correctly and efficiently with the help of Blaze-Persistence and PrimeFaces.
Displaying database contained data in tables is a basic requirement in almost any application and for a reasonably small amount of table rows per user, it is indeed very easy to implement: load all relevant data from the database - send it to the client - display it.
However, in most scenarios it is not possible to sufficiently restrict the amount of table rows and thus, with the previous approach the application would potentially load hundreds or thousands of records from the database, transfer it to the client and display it in one huge table. This is impractical both from a performance and from a usability and UX standpoint.
The standard approach of solving this issue is to extend the application to support paginated display of data. In this regard it is crucial that the pagination is performed on the database. This minimizes the total amount of data that needs to be transferred and consequently also reduces the load times.
After dynamic filtering and sorting of data is added to the equation, a seemingly easy task turns out to be quite complex. Since we want pagination to happen on the database we also need sorting and filtering to be taken care of by the database. Otherwise we cannot guarantee the completeness of the displayed data.
Implementing a paginated table on the UI side is not too difficult e.g. using the PrimeFaces data table component. It comes with all the required features:
-
user defined filtering
-
user defined dynamic sorting
-
user defined pagination
-
optionally backed by a lazy data model which provides a way of loading just the data needed for the current page
All what is left to do is to implement a lazy data model that combines the current state of pagination, sorting and filtering into a database query that fetches exactly the current page’s rows from the database. This is where Blaze-Persistence comes into play. Let me show you how this can be accomplished.
Example
Feel free to check out the full source code of the runnable example on Github. For instructions on how to run it, have a look into the README.
Let us assume a simple entity model - for the sake of compactness I skip the getters/setters along with some JPA annotations:
@Entity public class Cat { private Long id; private String name; private Cat parent; private Long age; }
We do not want to display the entity as-is in our data table so we define an entity view CatTableRowView
to describe our data table row model.
For an introduction on entity views check out this blog post by Christian Beikov. |
@EntityView(Cat.class) public interface CatTableRowView extends IdHolderView<Long> { @AttributeFilter(ContainsFilter.class) String getName(); Long getAge(); @AttributeFilter(ContainsFilter.class) @Mapping("parent.name") String getParentName(); @MappingSubquery(NumChildrenSubqueryProvider.class) Long getNumChildren(); class NumChildrenSubqueryProvider implements SubqueryProvider { @Override public <T> T createSubquery(SubqueryInitiator<T> sqInit) { return sqInit.from(Cat.class) .select("COUNT(*)") .where("parent.id").eqExpression("OUTER(id)") .end(); } } }
In the following we are going to directly use the defined attribute filters on the CatTableRowView to filter the data table. |
Here is how we define the data table in our JSF view:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:ui="http://xmlns.jcp.org/jsf/facelets" xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:p="http://primefaces.org/ui"> <f:view> <h:head/> <h:body> <h:form> <p:dataTable lazy="true" value="#{catDataModel}" var="cat" paginator="true" first="0" rows="10" paginatorPosition="top" paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}" rowsPerPageTemplate="10,25,50,100" filterEvent="enter" disabledSelection="true"> <p:column headerText="Name" filterBy="#{cat.name}" filterMatchMode="contains" filterable="true"> <h:outputText value="#{cat.name}"/> </p:column> <p:column headerText="Age" sortBy="#{cat.age}"> <h:outputText value="#{cat.age}"/> </p:column> <p:column headerText="Parent Name" filterBy="#{cat.parentName}" filterMatchMode="contains"> <h:outputText value="#{cat.parentName}"/> </p:column> <p:column headerText="Children" sortBy="#{cat.numChildren}"> <h:outputText value="#{cat.numChildren}"/> </p:column> </p:dataTable> </h:form> </h:body> </f:view> </html>
The most important part in this view definition is the use of lazy=true
and value="#{catDataModel}"
. The former tells PrimeFaces that the data model subsequently supplied via the value
attribute is a org.primefaces.model.LazyDataModel
so that the data table will indeed load the data lazily. Note that the #{catDataModel}
reference provides table row data of type CatTableRowView
so the row variable defined via var="cat"
also refers to instances of this type. Thus, when we write sortBy="#{cat.numChildren}"
we directly reference the numChildren
property in CatTableRowView
.
Most of the logic in the LazyDataModel
implementation can be abstracted in an AbstractLazyDataModel
which I will cover in excerpts in the following.
What we basically need to do is to implement the variants of load
that are provided by org.primefaces.model.LazyDataModel
:
@Override @SuppressWarnings("unchecked") public List<T> load( int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters) { // Notice that sort functions will only work with multisort enabled on // the data table final PagedList<T> list; if (sortField == null) { list = load(first, pageSize, Collections.EMPTY_LIST, filters); } else { list = load( first, pageSize, Arrays.asList(new SortMeta( null, sortField, sortOrder, null )), filters); } return list; } @Override public PagedList<T> load( int first, int pageSize, List<SortMeta> multiSortMeta, Map<String, Object> filters) { Object currentRequestContext = FacesContext.getCurrentInstance(); // prevent duplicate data fetching from the database // within the same request if (requestContext == currentRequestContext && entityData != null && entityData.getMaxResults() == pageSize && multiSortMeta.isEmpty() && filters.isEmpty()) { return entityData; } requestContext = currentRequestContext; if(hasSortingChanged(multiSortMeta)){ // we have to reset the keyset if the sorting changes keysetPage = null; } oldSortMeta = new ArrayList<>(multiSortMeta); entityData = getEntityData(first, pageSize, multiSortMeta, filters); setRowCount((int) entityData.getTotalSize()); keysetPage = entityData.getKeysetPage(); return entityData; }
The first variant of load
is basically delegated to the latter variant which in turn calls getEntityData
with the paging, sorting and filtering information. Since we need to reset the keysetPage
when the sorting changes, we store the sorting information for the next time the data model loads data.
Once the data for the current page has been retrieved we need to set the current row count on the data model. We also store the current keyset page for keyset pagination.
protected PagedList<T> getEntityData( int startRow, int rowsPerPage, List<SortMeta> multiSortMeta, Map<String, Object> filterMap) { return getEntityData(createSettings( viewClass, startRow, rowsPerPage, multiSortMeta, filterMap )); } /** * Implement this method to create a database query using the supplied entity view settings. * * @param setting the entity view settings that should be used for the query * @return the query results */ protected abstract PagedList<T> getEntityData(EntityViewSetting<T, PaginatedCriteriaBuilder<T>> setting);
Inside getEntityData
we construct entity view settings from the paging, sorting and filtering information and pass the settings to a variant of getEntityData
that needs to be implemented by the user for each concrete data model.
/** * This method creates entity view settings according to * the current state of pagination, filtering and sorting * in the data table. * * This implementation transparently uses keyset pagination * if possible. * * @param modelClass entity view class * @param startRow page start row * @param rowsPerPage page size * @param multiSortMeta current sort metadata * @param filterMap current filter settings * @return */ protected EntityViewSetting<T, PaginatedCriteriaBuilder<T>> createSettings( Class<T> modelClass, int startRow, int rowsPerPage, List<SortMeta> multiSortMeta, Map<String, Object> filterMap) { EntityViewSetting<T, PaginatedCriteriaBuilder<T>> setting = EntityViewSetting.create( modelClass, startRow, rowsPerPage ); setting.withKeysetPage(keysetPage); applyFilters(setting, filterMap); applySorters(setting, multiSortMeta); return setting; } protected void applyFilters( EntityViewSetting<?, ?> setting, Map<String, Object> filterMap) { for (Map.Entry<String, Object> entry : filterMap.entrySet()) { String attributeName = getAttributeName(entry.getKey()); Object filterValue = entry.getValue(); String filterString; if (filterValue == null) { filterString = null; } else if (filterValue instanceof String) { filterString = filterValue.toString(); } else { throw new IllegalArgumentException( "Unsupported filter value [" + filterValue + "], only strings are supported!" ); } setting.addAttributeFilter(attributeName, filterString); } } protected void applySorters( EntityViewSetting<?, ?> setting, List<SortMeta> multiSortMeta) { if (multiSortMeta != null && !multiSortMeta.isEmpty()) { for (SortMeta meta : multiSortMeta) { if (meta.getSortOrder() == SortOrder.UNSORTED) { continue; } String attributeName = getAttributeName(meta.getSortField()); Sorter sorter = meta.getSortOrder() == SortOrder.ASCENDING ? Sorters.ascending() : Sorters.descending(); setting.addAttributeSorter(attributeName, sorter); } } }
createSettings
applies the keyset page of a previous query to the settings before applying the sorting and filtering metadata which is also straightforward. This means that the pagination will be performed using the keyset pagination mode.
For more details about the pagination modes supported by blaze-persistence check out my introductory blog post about pagination with Blaze-Persistence. |
applyFilters
adds attribute filters to the entity view based on the filter settings of the data table. Attribute filters directly translate into a WHERE clause in the resulting JPQL/SQL query.
applySorters
adds attribute sorters to the entity view settings based on the sort metadata of the data table. Such sorters directly translate into a ORDER BY clause in the resulting JPQL/SQL query, hence sorting is also performed very efficiently.
For more information about the attribute filters and attribute sorters API please consult the blaze-persistence documentation. |
If row selection is enabled in the data table, the model also needs to be able to handle row key conversion. For this purpose, it is handy if the type parameter in the LazyDataModel implementation is bounded by a common id holder type which allows to implement the required getRowKey
and getRowData
methods within the AbstractLazyDataModel
:
@Override public Object getRowKey(T object) { return object.getId(); } @Override public T getRowData(String rowKey) { Serializable id = getIdFromRowKey(rowKey); for (T row : entityData) { if (row.getId().equals(id)) { return row; } } return null; } protected abstract Serializable getIdFromRowKey(String rowKey);
Finally, the concrete CatDataModel remains quite compact because most of the work is performed in the AbstractLazyDataModel
.
@javax.faces.view.ViewScoped @Named public class CatDataModel extends AbstractLazyDataModel<CatTableRowView> { @Inject private CriteriaBuilderFactory cbf; @Inject private EntityViewManager evm; @Inject private EntityManager em; @Override protected PagedList<CatTableRowView> getEntityData( EntityViewSetting<CatTableRowView, PaginatedCriteriaBuilder<CatTableRowView>> setting) { return evm.applySetting(setting, cbf.create(em, Cat.class)) .orderByAsc("id") .getResultList(); } @Override protected Serializable getIdFromRowKey(String rowKey) { return Long.valueOf(rowKey); } }
All this implementation does is to construct a criteria builder for the right entity class and apply the supplied readily configured entity view settings for the current data table state to it. Since Blaze-Persistence requires
-
at least one unique order by item
-
the last order by item to be unique
we add an .orderByAsc("id")
after applying the settings which ensures that both conditions are satisfied.
Since the AbstractLazyDataModel
needs to save state across user requests, the concrete data model needs to be scoped accordingly. In this example I use the standard javax.faces.view.ViewScoped
but other view scopes will most likely work as well. The stored state managed by AbstractLazyDataModel
includes:
-
keysetPage
- the keysetPage extracted from the last query which will be used for pagination in a subsequent query. -
entityData
,requestContext
- this transient data is used for request caching, i.e. if the data model is invoked multiple times within the same request, the query is performed just once and any subsequent invocation returns the cached page. -
oldSortMeta
- used to detect changes of sort modes. This is needed because the keyset page needs to be reset when sorting changes.
Please note that for the sake of compactness I implemented the query directly inside the concrete data model. In a real world application you would probably put this into some sort of repository / DAO.
Here is a screenshot of the running demo:
Conclusion
In this blog post I presented a way to combine the powers of PrimeFaces and Blaze-Persistence to efficiently and correctly implement data table pagination. The presented approach utilizes the org.primefaces.model.LazyDataModel
as an integration point to construct a Blaze-Persistence criteria builder for fetching entity views from the database based on the current pagination, filtering and sorting metadata. I am very happy to receive any comments with questions, remarks or suggestions for improvements.