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 setFirstResult e setMaxResults 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
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
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