Introduction
JSF and Primefaces is a great solution for building web-based user interfaces. It enables you to implement your requirements in a user-friendly way with moderate efforts. Especially, the Primefaces Showcase offers you a decent way to take a peek at all the nice components complemented with numerous useful code examples.
In this article I will elaborate on the DataTable component and describe how to factilitate Spring Data in order to handle huge datasets.
The examples for this article will be based on Java 8 and the following libraries:
- Spring Boot 1.4.1
- Spring Data JPA 1.12
- JSF 2.2
- Primefaces 6.0
Primefaces DataTable
Primefaces offers an extensive collection of numerous useful components. One of the most frequently used component --especially in CRUD applications-- is the DataTable. With DataTable you are able to design powerful data views with, among others, the following features:- Sorting (even multiple rows)
- Filtering via header input for each column
- Paging
- Selection columns
- Sticky columns
- Context menu
- and many more
Figure 1: DataTable in action |
The corresponding xhtml for the user interface is as simple as this:
<h:body styleClass="login">
<h:form id="form">
<p:dataTable value="#{userBean.users}" var="user" rows="10"
paginator="true" paginatorTemplate="{RowsPerPageDropdown} {FirstPageLink}
{PreviousPageLink} {CurrentPageReport} {NextPageLink} {LastPageLink}"
rowsPerPageTemplate="5,10,15" selectionMode="single" id="userTable">
<p:column headerText="Id" sortBy="#{user.id}" filterBy="#{user.id}">
<h:outputText value="#{user.id}" />
</p:column>
<p:column headerText="Login" sortBy="#{user.login}" filterBy="#{user.login}">
<h:outputText value="#{user.login}" />
</p:column>
<p:column headerText="Firstname" sortBy="#{user.firstName}"
filterBy="#{user.firstName}">
<h:outputText value="#{user.firstName}" />
</p:column>
<p:column headerText="Lastname" sortBy="#{user.lastName}"
filterBy="#{user.lastName}">
<h:outputText value="#{user.lastName}" />
</p:column>
<p:column headerText="DayOfBirth" sortBy="#{user.dayOfBirth}"
filterBy="#{user.dayOfBirth}">
<h:outputText value="#{user.dayOfBirth}" />
</p:column>
<p:column headerText="Manager" sortBy="#{user.manager.lastName}"
filterBy="#{user.manager.lastName}">
<h:outputText value="#{user.manager.lastName}" />
</p:column>
</p:dataTable>
</h:form>
</h:body>The datatable tag renders the aforesaid data table with the given columns Login, Firstname, Lastname, DayOfBirth and Manager. The value attribute points to the managed bean named userBean and the method getUsers will return the list of users to be rendered. The var attribute defines to store each user in a variable named user. Rows defines that the table should render at most 10 rows per page whereas the following attributes paginator, paginatorTemplate define the paging. For each column tag the sortBy respectively filterBy define to sort/filter by the specified attributes of a user.
The referenced managed bean looks like this:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public class UserBean {
@Autowired
private UserService userService;
public List<User> getUsers() {
return userService.findAll()
}
}
Please notice that this is a Spring bean instead of a JSF ManagedBean. It is implicitly made available by the name userBean. It defines the getUsers() method to return all users from the database.
In my experience, you can really quickly design a user-friendly data view with this set of features. However, once the number of data items you are rendering is getting huge, you will face performance problems because the filtering, sorting and paging actions will always operate on the whole data set, i.e. you must first load all data items from database to do so.
Fortunately, Primefaces offers a solution to this challenge by making a DataTable lazy.
Lazy DataTable
To get a lazy DataTable, you must set the lazy attribute of the dataTable component to true and in your managed bean, instead of returning a list with the data items, you must return an instance of LazyDataModel. The following code snippet demonstrates this:
<p:dataTable lazy="true" ...>
If you do so you, you will have to, instead of returning a list of your data in the managed bean, return a data model which extends LazyDataModel. Thus, you need to adapt the managed bean as follows:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public class UserBean {
@Autowired
private UserService userService;
private UserLazyDataModel users;
@PostConstruct
private void init() {
this.users = new UserLazyDataModel(userService);
}
public UserLazyDataModel getUsers() {
return users;
}
}
As you can see I've introduced an attribute of type UserLazyDataModel instead of the List<User>. Let's have a look at this lazy data model class:
public class UserLazyDataModel extends LazyDataModel<User>{
private final transient UserService userService;
public UserLazyDataModel(UserService userService) {
this.userService = userService;
}
@Override
public List<User> load(int first, int pageSize, String sortField,
SortOrder sortOrder, Map<String, Object> filters) {
SortOrder sortOrder, Map<String, Object> filters) {
//TODO Implement this
}
@Override
public List<User> load(int first, int pageSize, List<SortMeta> multiSortMeta,
Map<String, Object> filters) {
//TODO Implement this
}
}
You must override two methods: the first one is required for simple one-row-sorting and the second one is required if our data table implements multi-row-sorting (attribute sortMode="multiple" in dataTable; can be used with Command key + click on column header). The parameters of these methods are:
- first - the first element to be shown (compare to OFFSET in SQL)
- pageSize - how many elements shall be shown (compare to LIMIT in SQL)
- sortField - The name of the field to sort by e.g. firstName
- sortOrder - The direction of sort (ASCENDING, DESCENDING, UNSORTED)
- filters - A map with key value pairs; key = name of field and value = filter value.
- multiSortMeta - A list of sort meta (each entry in this list contains sort order and sort field)
The load method will be called by Primefaces whenever the user navigates to another page, sorts or filters the data of the table. To be able to deal with huge data sets this task should be delegated to the underlying database. To achieve this, I will first elaborate on how to write a service that fulfils the given requirements. This is when Spring data comes into play.
Using Spring Data JPA for Paging, Sorting and Filtering
In the following, I will explain the backend from service to repository. The service must provide an interface serving our requirements from the LazyDataModel.load() methods. The signature can be seen in the next example:
@Service
public class UserService {
public Page<User> findByFilter(Map<String, String> filters, Pageable pageable) {
//TODO Implement this
}
}
This service implementation provides one method to find users by the given filter data. Hence, I've introduced the filters attribute of type Map. So far so good, but what about sorting and paging? This is covered by the second parameter pageable of type org.springframework.data.domain.Pageable. The pageable is a standard spring data container allowing to define multiple sort criteria and paging. The return type Page is also such a Spring data container which encapsulates the actual data enriched with paging meta data (for instance which page has been loaded and how big the page actually is). A clear advantage to rely on these classes is that you can pass them as arguments/return values to standard repository methods. This enables Spring's out-of-the-box paging and sorting.
Spring Repository
To understand paging and sorting you should have a look at the Spring repository, a class which encapsulates all data base operations for a single entity class (in this case User):
public interface UserRepository extends PagingAndSortingRepository<User, Long>,
JpaSpecificationExecutor<User>{
public Page<User> findByLogin(String login, Pageable pageable);
}
Please notice, that the repository is just an interface. You don't have to (although you can indeed, if you need custom code) implement this interface yourself. Spring will do this for you (at runtime). For example, the findByLogin method shown here follows the Spring data naming conventions and thus will produce a database query which will load users by their login. Because this method defines an additional page parameter of type Pageable the result will be paged and returned in the Page container object.
The UserRepository interface extends two Spring data interfaces: PagingAndSortingRepository and JpaSpecificationExecutor. The first one will mark this interface as repository and provide standard methods such as findAll. The second interface will contribute more advanced methods allowing to parameterize methods such as findAll with so called Specifications. Specifications provide an abstraction layer on top of the JPA criteria API and help you to modularise your criteria queries into reusable combinable components. I will demonstrate the reuse of specifications later.
The UserRepository interface extends two Spring data interfaces: PagingAndSortingRepository and JpaSpecificationExecutor. The first one will mark this interface as repository and provide standard methods such as findAll. The second interface will contribute more advanced methods allowing to parameterize methods such as findAll with so called Specifications. Specifications provide an abstraction layer on top of the JPA criteria API and help you to modularise your criteria queries into reusable combinable components. I will demonstrate the reuse of specifications later.
Service Implementation
Let's have an elaborate look on how to use specifications to implement the desired service.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public Page<User> findByFilter(Map<String, String> filters, Pageable pageable) {
return userRepository.findAll(getFilterSpecification(filters), pageable);
}
private Specification<User> getFilterSpecification(
Map<String, String> filterValues) {
Map<String, String> filterValues) {
return (Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
Optional<Predicate> predicate = filterValues.entrySet().stream()
.filter(v -> v.getValue() != null && v.getValue().length() > 0)
.map(entry -> {
Path<?> path = root;
String key = entry.getKey();
if (entry.getKey().contains(".")) {
String[] splitKey = entry.getKey().split("\\.");
path = root.join(splitKey[0]);
key = splitKey[1];
}
return builder.like(path.get(key).as(String.class),
"%" + entry.getValue() + "%");
"%" + entry.getValue() + "%");
})
.collect(Collectors.reducing((a, b) -> builder.and(a, b)));
return predicate.orElseGet(() -> alwaysTrue(builder));
};
}
private Predicate alwaysTrue(CriteriaBuilder builder) {
return builder.isTrue(builder.literal(true));
}
}
Ok stop. This needs quite some explanation. First, you will have noticed that I've injected the UserRepository interface into the service. Second, I've made use of the repository to implement findByFilter. I call the findAll method which takes as first parameter a specification and as second argument a pageable. This is a standard method provided by the JpaSpecificationExecutor interface. The return value matches the required Page<User> type. This looks quite simple. What is still left to do is to instantiate the specification in order to actually do the filtering based on the given filterValues map. The code for defining the specification object has been extracted into the getFilterSpecification method. This method returns, as expected, an instance of Specification. Specification is an interface with just one method:
- Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
This is beneficial because it adheres to the concept of functional interfaces and consequently you can use a lambda expression instead of defining an anonymous inner class. The main task of the lambda is to operate on the filter values map. To process data in the map, stream() is called on the entry set. Then, filter() is used to remove empty values from the map. After that map() is called to transform the entries in the entry set into a stream of predicates.
Let's have a more elaborate look at what map() is actually doing. In the first part of the code, I deal with attributes that are not directly part of the user entity but come from other associated entities which have been mapped with @OneToMany or @ManyToOne). For example, one could have specified user.manager.name as the filter attribute key. In this case, the code extracts this first attribute after the dot and joins the respective entity.
Then, I build the desired like predicate by using the attribute path of the entity which can point to car.name or just login as a direct attribute of user. The second argument specifies % + value + % as the value to be compare with the attribute described by path.
After the mapping process you will receive a stream of like predicates. What yo need, however, is a single predicate. Hence you reduce() the stream by combining each predicate in the stream with and (which returns a new predicate with the conjunction of both input arguments). Finally, this will combine all elements of the list into a single predicate.
Let's have a more elaborate look at what map() is actually doing. In the first part of the code, I deal with attributes that are not directly part of the user entity but come from other associated entities which have been mapped with @OneToMany or @ManyToOne). For example, one could have specified user.manager.name as the filter attribute key. In this case, the code extracts this first attribute after the dot and joins the respective entity.
Then, I build the desired like predicate by using the attribute path of the entity which can point to car.name or just login as a direct attribute of user. The second argument specifies % + value + % as the value to be compare with the attribute described by path.
After the mapping process you will receive a stream of like predicates. What yo need, however, is a single predicate. Hence you reduce() the stream by combining each predicate in the stream with and (which returns a new predicate with the conjunction of both input arguments). Finally, this will combine all elements of the list into a single predicate.
The result of this stream modification is however not a Predicate but an Optional<Predicate> because our input filterValues map could have been empty or does not contain non-empty values. Similar to an SQL query, if you don't provide any where clause the query should return all elements. because no filtering takes place. Thus, I return the True predicate, if the optional is empty.
The great benefit of the implementation of this filtering in form of a specification is that you could have combined the specification with other specifications that you could have written. Instead of directly passing the specification into the findAll method you could have used org.springframework.data.jpa.domain.Specifications.where as the following snippet demonstrates:
public Page<User> findByFilter(Map<String, String> filters, Pageable pageable) {
return userRepository.findAll(
where(getFilterSpecification(filters))
.and(anotherSpecification), pageable);
}
Really powerful, isn't it? Now let's finally have a look on how to integrate our service into the lazy data model.
Integration of Service into LazyDataModel
public class UserLazyDataModel extends LazyDataModel<User>{
private final transient UserService userService;
private static final SortOrder DEFAULT_SORT_ORDER = SortOrder.ASCENDING;
private static final String DEFAULT_SORT_FIELD = "login";
public UserLazyDataModel(UserService userService) {
this.userService = userService;
}
@Override
public List<User> load(int first, int pageSize, String sortField,
SortOrder sortOrder, Map<String, Object> filters) {
SortOrder sortOrder, Map<String, Object> filters) {
Sort sort = null;
if (sortField != null) {
sort = new Sort(getDirection(
sortOrder != null ? sortOrder : DEFAULT_SORT_ORDER), sortField);
} else if (DEFAULT_SORT_FIELD != null) {
sort = new Sort(getDirection(
sortOrder != null ? sortOrder : DEFAULT_SORT_ORDER), DEFAULT_SORT_FIELD);
}
return filterAndSort(first, pageSize, filters, sort);
}
@Override
public List<User> load(int first, int pageSize, List<SortMeta> multiSortMeta,
Map<String, Object> filters) {
Sort sort = new Sort(getDirection(DEFAULT_SORT_ORDER), DEFAULT_SORT_FIELD);
if (multiSortMeta != null) {
List<Order> orders = multiSortMeta.stream()
.map(m -> new Order(getDirection(
m.getSortOrder() != null ? m.getSortOrder() : DEFAULT_SORT_ORDER),
m.getSortField()))
.collect(Collectors.toList());
sort = new Sort(orders);
}
return filterAndSort(first, pageSize, filters, sort);
}
private List<User> filterAndSort(int first, int pageSize,
Map<String, Object> filters, Sort sort) {
Map<String, Object> filters, Sort sort) {
Map<String, String> filtersMap = filters.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString()));
Page<User> page = userService.findByFilter(filtersMap,
new PageRequest(first / pageSize, pageSize, sort));
new PageRequest(first / pageSize, pageSize, sort));
this.setRowCount(((Number) page.getTotalElements()).intValue());
this.setWrappedData(page.getContent());
return page.getContent();
}
private static Direction getDirection(SortOrder order) {
switch (order) {
case ASCENDING:
return Direction.ASC;
case DESCENDING:
return Direction.DESC;
case UNSORTED:
default:
return null;
}
}
}
The code above mainly implements a mapping from Primefaces sorting and paging model into the Spring data one. The getDirection method on the bottom transforms Primefaces SortOrder into Spring Direction. The onLoad methods transform the given sort criteria into Spring Sort interface. The filterAndSort method finally calls the service method I have written and injects the Sort object and the paging data into the PageRequest object which implements Pageable. To match the List<User> return type the result, which is a Page, must be transformed. The list can easily be extracted from the content attribute of page and the row count is given by the totalElements meta data from Page.
Conclusion & Limitations
In this article I've demonstrated how to use the Primefaces DataTable component to render huge data sets by delegating sorting, filtering and paging to the database. I've used Spring data to implement this at the database level by creating a dynamic query with JPA Criteria API. An interesting aspect in the course of this implementation is the use of specifications because they facilitate the modularisation of your dynamic Criteria-API-based queries.
Please notice that in this example only one level of attribute navigation in filter values is supported. In order to support arbitrary levels of navigation one has to extend the dynamic attribute join in the filter UserService.getFilterSpecification to be applied recursively.
Please notice that in this example only one level of attribute navigation in filter values is supported. In order to support arbitrary levels of navigation one has to extend the dynamic attribute join in the filter UserService.getFilterSpecification to be applied recursively.
I hope you found this article helpful. I am looking forward to your feedback! The code examples can be found here https://github.com/Javatar81/code-examples/tree/master/lazydatatable.