segunda-feira, 13 de maio de 2013

DataTable - Lazy Loading com Primefaces

Um problema bem comum com quem trabalha com aplicações é web são as grids com paginação em memória. Para uma consulta que não retorna muitos dados o problema não é muito grande, mas em um sistema em produção é muito provável que paginar em memória se torne um gargalo rapidamente.

O Seam criou uma solução bastante elegante e prática que lida bem com filtros dinâmicos. Com base em minha experiencia com o Seam, decidi implementar algo com funcionalidade parecida com JSF2 e Primefaces.

O DataTable do Primefaces suporta Lazy Loading através da classe tipada org.primefaces.model.LazyDataModel, mas o exemplo deles é feito em cima de uma lista pronta em vez de um consulta EJB-QL. Vamos utilizar os métodos setFirstResultsetMaxResults para trazer o range correto da consulta de acordo com a paginação.

Para permitir paginação é preciso saber o total de elementos que a consulta traria e para isto será utilizado uma consulta com count. Na solução do Seam esta consulta é criada automaticamente e funciona bem para a maioria dos casos, mas é possível criar manualmente a consulta. Para simplificar as coisas nesta primeira versão, a consulta do count deve ser criada manualmente.

Para os filtros o funcionamento vai ser semelhante ao do Seam, mas sem a restrição de permitir apenas uma expressão el por filtro.

A classe abstrata tipada DataList estende a LazyDataModel fazendo todo o tratamento. Criei a classe como abstrata para que seja obrigatória a implementação dos métodos abaixo:

protected Map<String, DataListFilter> getFiltros();
protected abstract String getSqlList();
protected abstract String getSqlCount();

O principal método da classe LazyDataModel  é o public List load(int first, int pageSize, String sortField, SortOrder sortOrder, Map filters). É necessário sobrescrever, pois ele sempre retorna UnsupportedOperationException("Lazy loading is not implemented.").

O DataTable do Primefaces possui sistema de ordenação e de filtros e eles são repassados por esses parâmetros, juntamente com os dados referentes a paginação:
int first - Equivalente ao OFFSET
int pageSize - Equivalente ao LIMIT
String sortField - Nome do campo usado na ordenação
SortOrder sortOrder - Enum que indica se a ordenação será ASC ou DESC
Map filters - Nessa map a key indica o nome do campo e o value o valor.

Como o sistema de filtro do método load suporta apenas Strings, fiz um suporte para filtros com valores dinâmicos baseados em 'el'.

Para definir o filtro é preciso apenas sobrescrever o método getFiltros() como pode ser visto no exemplo abaixo.
 
 protected Map<String, DataListFilter> getFiltros() {
  filtros = new HashMap<String, DataListFilter>();
  filtros.put("nome", new DataListFilter("AND o.nome like #{empty estadoDataList.nomeEstado ? null : estadoDataList.nomeEstado.concat('%')}"));
  filtros.put("sigla", new DataListFilter("AND o.sigla = #{estadoDataList.siglaEstado}"));
  return filtros;
 }

A classe DataListFilter será a responsável pelas operações em cima do filtro. Basicamente ela precisa fazer o parse do filtro para encontrar todas as expressões #{el} e transformar em um parâmetro da Query. Por exemplo para o filtro "AND o.sigla = #{estadoDataList.siglaEstado}" será encontrada a el #{estadoDataList.siglaEstado} e a consulta EJB-QL será AND o.sigla = :estadoDataList_siglaEstado.

Outra coisa importante é avalizar todas as expressões do filtro e caso todas retornem valor o método isPopulatedExpressions() retornará true. Isto é usado pela classe DataList para saber se o filtro deve ser adicionado na String da Query bem como no momento de aplicar os parâmetros.

O regex que identifica e captura as expressões é bastante simples e utiliza grupos para poder obter as informações de maneira mais fácil: (\#\{(.*?)\})

Para avaliar as expressões criei o método abaixo, ele é bastante útil em uma aplicação JSF:
 
 private <C> C eval(String el, Class<C> classReturn) {
  FacesContext facesContext = FacesContext.getCurrentInstance();
  Application application = facesContext.getApplication();
  ExpressionFactory factory = application.getExpressionFactory();
  ELContext elContext = facesContext.getELContext();
  ValueExpression valueExpression = factory.createValueExpression(elContext, el, classReturn);
  return (C) valueExpression.getValue(elContext);
 }


Na pratica a criação do ManagedBean referente do DaraTable fica simples:
 
package br.rodrigo.dao.list;

import java.util.HashMap;
import java.util.Map;

import javax.enterprise.context.RequestScoped;
import javax.faces.bean.ManagedBean;
import javax.inject.Inject;
import javax.persistence.EntityManager;

import br.rodrigo.dao.DataList;
import br.rodrigo.dao.DataListFilter;
import br.rodrigo.model.Estado;

@ManagedBean
@RequestScoped
public class EstadoDataList extends DataList<Estado> {
 
 private static final String SQL_LIST = "select o from Estado o";
 private static final String SQL_COUNT = "select count(o) from Estado o";
 
 private Map<String, DataListFilter> filtros;
 
 private String nomeEstado;
 private String siglaEstado;
 
 @Inject
 private EntityManager em;

 private static final long serialVersionUID = 1L;

 public String getNomeEstado() {
  return nomeEstado;
 }
 
 public void setNomeEstado(String nomeEstado) {
  this.nomeEstado = nomeEstado;
 }
 
 public String getSiglaEstado() {
  return siglaEstado;
 }
 
 public void setSiglaEstado(String siglaEstado) {
  this.siglaEstado = siglaEstado;
 }

 @Override
 protected Map<String, DataListFilter> getFiltros() {
  filtros = new HashMap<String, DataListFilter>();
  filtros.put("nome", new DataListFilter("AND o.nome like #{empty estadoDataList.nomeEstado ? null : estadoDataList.nomeEstado.concat('%')}"));
  filtros.put("sigla", new DataListFilter("AND o.sigla = #{estadoDataList.siglaEstado}"));
  return filtros;
 }

 @Override
 protected String getSqlList() {
  return SQL_LIST;
 }

 @Override
 protected String getSqlCount() {
  return SQL_COUNT;
 }
}

No arquivo xhtml o código ficaria:



Fiz esse mecanismo como prova de conceito e exercício, embora tudo esteja funcional é preciso trabalhar melhor as classes. Abaixo vocês podem fazer o download das classes e do projeto eclipse. Utilizei como servidor o Jboss 7.0.1 final e como IDE o Eclipse Juno e Jboss tools instalado via Eclipse Market.

DataList
DataListFilter
EstadoDataList
StringUtil
ScriptBanco
Página com a grid

Projeto