Paginator

Objetivo

Separar o conteúdo em grupos de itens (páginas) e controlar a navegação entre as páginas.

Propósito

É 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.

Implementação

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.

Discussão

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.

Padrões associados

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.

Scroll to Top