public interface PaginatorModel<T> {
public long totalCount();
public Collection<T> readRange(int startAt, int itemsPerPage);
}
Separar o conteúdo em grupos de itens (páginas) e controlar a navegação entre as páginas.
É comum em diversos diversos tipos de aplicações apresentarmos ao usuário uma lista de alguma entidade. Em sistemas orientados a cadastro é comum listarmos todos as instancias já cadastradas (normalmente com possibilidade de filtro), em sistemas de e-comerce é normal apresentar a lista de produtos. Rapidamente nos damos conta que estas listas podem ser muitas vezes ser gigantes. Mesmo utilizando filtros o usuário pode ser bombardeado com milhares de resultados para visualização. Processar esta quantidade de informação se torna rapidamente onerosa para o sistema e a performance diminui drasticamente.
A ideia do padrão Paginator é agregar os itens encontrados em conjuntos: páginas. Cada página contém um numero limitado de itens e apenas uma página é carregada por vez. Desta forma o sistema irá apenas processar uma quantidade limitada de itens em cada tela e nunca terá que ler todos os itens de uma só vez.
A paginação não se limita apenas a facilitar a apresentação das nossas listagens, ela pode ser usada como acelerador em processos batch em que não queremos ler cada itens por vez, e também não queremos ler todos os itens de existem de uma só vez. Queremos ler os itens em pacotes ( batches), e páginas são uma forma de empacotar os itens.
Não existe um padrão para a implementação de um paginador devido ao problema da fonte de dados. Às vezes queremos paginar de uma lista, outras queremos paginar diretamente do banco e outras queremos paginar utilizando um serviço externo como um web service REST, por exemplo. A implementação seguinte começa por encapsular o problema de obter a fonte de dados usando uma interface de modelo de dados.
public interface PaginatorModel<T> {
public long totalCount();
public Collection<T> readRange(int startAt, int itemsPerPage);
}
O método totalCount
nos dirá quantos itens existem na fonte de dados. Esta informação é vital para calcular quantas páginas existem. O método readRange
irá ler os itens começando em uma posição startAt
e obtendo no máximo uma quantidade de itemsPerPage
que será retornada em uma coleção dos objetos pretendidos.
Podemos passar agora ao objeto Paginator em si.
public class Paginator<T>
private final PaginatorModel<T> model;
private final int itemsPerPage;
private int currentPageIndex = -1;
public Paginator(PaginatorModel<T> model, int itemsPerPage){
this.model = model;
this.itemsPerPage = itemsPerPage;
}
public int getPageCount(){
final long count = model.totalCount();
return (int)(count / itemsPerPage + (count % itemsPerPage == 0 ? 0: 1));
}
public int getCurrentPageIndex(){
return currentPageIndex;
}
public void setCurrentPageIndex(int currentPageIndex){
this.currentPageIndex = currentPageIndex;
}
public Collection<T> getPageItens(){
if (currentPageIndex < 1 || currentPageIndex > this.getPageCount() + 1 ) {
return Collections.emptySet();
}
int startAt = (this.currentPageIndex-1) * this.itemsPerPage;
return model.getRange(startAt, itemsPerPage);
}
...
}
A classe Paginador recebe o PaginatorModel
que serve como ponte para os dados reais, e a quantidade de items por página. O método getPageCount
calcula a quantidade máxima de páginas. Fazemos isto em um método que usaremos bastante e de forma dinâmica (i.e. não gravamos o resultado em um campo) porque a quantidade de itens total retornada por model.totalCount
pode mudar.
Estabelecemos que a página corrente é sempre um página inexistente ( currentPageIndex = -1
) de forma semelhante a java.sql.ResultSet
e permitimos que o utilizador da classe escolha que página pretende usar. É o uso de setCurrentPageIndex
que realmente pagina os itens. Estabelecemos que a primeira página terá um índex 1
.
Para obter os dados utilizamos getPageItens
que calcula o ponto inicial da pesquisa e delega ao modelo para obter os dados realmente. Se estivermos posicionados em um página que não existe, o resultado é simplesmente uma coleção vazia. Este comportamento pode ser adequado às suas necessidades, lançado uma exceção, por exemplo.
Este é o mecanismo básico, mas realmente ser útil o objeto Paginator
tem que prover modo de navegar entre as páginas. Para isso, adicionamos os seguintes métodos:
public boolean hasNextPage(){
return this.currentPageIndex < this.getPageCount();
}
public boolean hasPreviousPage(){
return this.currentPageIndex > 1;
}
public boolean isFirstPage(){
return this.currentPageIndex == 1;
}
public boolean isLastPage(){
return this.getPageCount()==0 || this.currentPageIndex == this.getPageCount();
}
public void moveToFirstPage(){
this.moveToPage(1);
}
public void moveToLastPage(){
this.moveToPage(this.getPageCount());
}
public void moveToNextPage(){
int count = this.getPageCount();
this.moveToPage(this.currentPageIndex == count ? count : this.currentPageIndex +1);
}
public void moveToPreviousPage(){
this.moveToPage(this.currentPageIndex == 1 ? 1 : this.currentPageIndex -1);
}
public void moveToPage(int pageIndex){
this.currentPageIndex = pageIndex;
}
Esta é apenas uma implementação possível, existirão muitas outras conforme os constrangimentos que o seu projeto estiver sujeito. A implementação mostrada aqui tenta separar a responsabilidade de controlar a paginação da responsabilidade de obter os dados para a página.
Separar o controle de paginação da leitura dos dados é realmente a parte mais dificil da implementação do padrão. Implementar PaginatorModel
com base em um List
é trivial e o próprio List
inclui métodos como subList
e size
que mapeiam facilmente para os métodos em PaginatorModel
com base em um List
é trivial e o próprio List
inclui métodos como subList
e size
que mapeiam facilmente para os métodos em PaginatorModel
. Por outro lado, implementar a mesma interface para acessar um banco de dados não é tão simples.
Para utilizar um banco de dados primeiro o banco de dados, ele próprio, tem que suportar paginação, pois senão não haverá ganho real. Para ter suporte completo o banco precisa ter uma funcionalidade que permite escolher a partir de uma posição especifica ( _offset_ ) e um função que permita limitar o bloco que linhas retornada. Alguns bancos não têm capacidade de offset (SQL Server 2005, por exemplo) o que significa que embora possamos escolher os 10 primeiro elementos das terceira página, somos obrigados a carregar e a desprezar os primeiros 20 elementos.
Além disso as instruções de limite e offset são incluídas na frase SQL, o que significa que a frase será ligeiramente diferente a cada invocação. Aqui existem algumas opções trazidas pela orientação a objetos construindo a frase com objetos compostos em vez de String
usando padrões como Query Object e Interpreter.
Como já falamos Query Object e Interpreter são boas maneiras de encapsular a frase de consulta para poder injetar os limites e offsets. Uma outra possível de driblar o problema é fazer com que o DAO retorne um objeto Paginator
em vez do normal List
desta forma postergando realizar a query efetivamente e esperando até que sejam sabidos os limites da primeira pagina. A mesma técnica de postergação de pesquisa pode ser usada como Repository . Esta técnica pode-se tornar complexa em ambiente distribuído mas é funciona excelentemente em ambiente em que tudo acontece em uma única máquina virtual, como é o caso de muitas aplicações web.
A paginação é uma técnica que está interessada em ler poucos dados de uma vez e raramente lê todos os dados. O padrão Fast Lane Reader pode ser aplicado para aumentar ainda mais a eficiência da leitura diminuindo a memória necessária e praticamente trazendo o java.sql.ResultSet
até à camada de apresentação de forma encapsulada.