quinta-feira, 22 de novembro de 2007

Fazendo qualquer consulta ao banco de forma paginada.

Como eu imaginava, não tenho tempo para olhar muito isso e fazer novas postagens, mas juro que vou tentar melhorar hehe.

Já tem um tempo to devendo postar isso, é uma solução que fiz e uso para paginação no acesso a dados. Ela surgiu quando eu tinha que ler muitas tabelas e muitos registros de cada uma, o que gerava um OutOfMemory, então eu quis criar uma forma de que qualquer sql que eu executasse, ou até mesmo qualquer acesso a objetos (como por exemplo lendo de um XML, ou arquivo) que eu tivesse suporte a paginação de dados. Mas eu queria que isso fosse feito de forma transparente, ou seja, pudesse tratar esse retorno em for each do Java 5 por exemplo.

Não vou entrar em detalhes da implementação, mesmo porque não tem nada complexo, vou postar os códigos e um comentário simples apenas.

Esta é a interface de acesso que suporta paginação, todos meus objetos devem ser acessados a partir dessa interface, logo toda implementação que busca objetos (do banco, de arquivos, etc) implementa essa interface

public interface DataSet<T> extends Iterable<T> {

 List<T> list(int firstResult, int maxResults);

 List<T> listAll();

 int size();
}

O negócio é simples, apenas três métodos, um que exige suporte paginado, um que possibilita listar tudo e o ultimo que retorna o tamanho, além de estender Iterable (para eu que possa dar o for each). A próxima classe é justamente uma que implementa Iterable para poder ser usado em um DataSet:

public class DataSetIterator<T> implements Iterator<T> {

 private final DataSet<T> dataSet;
 
 private final int cacheSize;

 private List<T> cacheData;
 
 private int currentIt;
 
 private int totalReaded;

 public DataSetIterator(DataSet<T> dataSet, int cacheSize) {
  if (cacheSize <= 0 || dataSet == null) {
   throw new IllegalArgumentException();
  }
  this.dataSet = dataSet;
  this.cacheSize = cacheSize;
  this.currentIt = 0;
 }

 public boolean hasNext() {
  if (currentIt >= cacheSize || cacheData == null) {
   cacheData = dataSet.list(totalReaded, cacheSize);
   currentIt = 0;
  }
  return cacheData.size() > currentIt;
 }

 public T next() {
  if (hasNext()) {
   totalReaded++;
   return cacheData.get(currentIt++);
  } else {
   throw new NoSuchElementException();
  }
 }

 public void remove() {
  throw new UnsupportedOperationException();
 }
}

A próxima classe é só uma abstração de DataSet que implementa os métodos exigidos pela interface Iterable, no caso usando o Iterator criado anteriormente

public abstract class AbstractDataSet<T> implements DataSet<T> {
 public Iterator<T> iterator() {
  return new DataSetIterator<T>(this, 50);
 }
}

Agora vou criar duas classes que serão úteis para meus DataSet's que acessam o banco de dados. A primeira classe é justamente a implementação de um DataSet mas que lê dados uma List do Java, isso vai ser útil para caso seja utilizado o método listAll de algum DataSet que acessa o banco, nas próximas consultas ao mesmo DataSet não será mais lido do banco (pois já leu tudo) e sim de uma lista em memória (DataSet de lista).

public class ListDataSet<T> extends AbstractDataSet<T> {

 private final List<T> list;

 public ListDataSet(List<T> list) {
  if (list == null) {
   throw new IllegalArgumentException();
  }
  this.list = list;
 }

 public List<T> list(int firstResult, int maxResults) {
  if (firstResult > list.size()) {
   return new ArrayList<T>();
  }
  if (firstResult < 0 || maxResults <= 0) {
   return list;
  }
  int toIndex = Math.min(firstResult + maxResults, list.size());
  return list.subList(firstResult, toIndex);
 }

 public int size() {
  return list.size();
 }

 public List<T> listAll() {
  return list;
 }
}

A segunda é uma interface, sua função é como se fosse um Factory. Vai servir para obter o Criteria quando fizer um DataSet que usa Criteria do Hibernate ou obter a Query quando fizer um DataSet que usa Query do JPA.

public interface Provider<T> {
 
 T get();

}

Agora sim, finalmente os DataSet's que fazem acesso ao banco de dados. O primeiro serve para fazer consultas usando ejbql do JPA. Ele recebe dois providers (fabricas, como você achar melhor), um para obter a Query da consulta em si e outro para obter a Query que busca a quantidade de registros (um count da consulta não paginada).

public class JpaQueryDataSet<T> extends AbstractDataSet<T> {

 private Integer size;

 private ListDataSet<T> listDs;

 private final Provider<Query> queryProvider;

 private final Provider<Query> countQueryProvider;

 public JpaQueryDataSet(Provider<Query> queryProvider,
   Provider<Query> countQueryProvider) {
  if (queryProvider == null || countQueryProvider == null) {
   throw new IllegalArgumentException();
  }
  this.size = null;
  this.listDs = null;
  this.queryProvider = queryProvider;
  this.countQueryProvider = countQueryProvider;
 }

 @SuppressWarnings("unchecked")
 public List<T> list(int firstResult, int maxResults) {
  if (listDs != null) {
   return listDs.list(firstResult, maxResults);
  } else {
   final Query query = queryProvider.get();
   if (firstResult < 0 || maxResults <= 0) {
    final List<T> list = query.getResultList();
    size = list.size();
    listDs = new ListDataSet<T>(list);
    return list;
   } else {
    query.setFirstResult(firstResult);
    query.setMaxResults(maxResults);
    final List<T> list = query.getResultList();
    return list;
   }
  }
 }

 public int size() {
  if (size == null) {
   final Query query = countQueryProvider.get();
   size = ((Long) query.getSingleResult()).intValue();
  }
  return size;
 }

 public List<T> listAll() {
  return list(-1, -1);
 }
}

Esse segundo DataSet serve para fazer consultas usando criteria do Hibernate. Ele recebe um provider justamente para obter o Criteria:

public class HibernateCriteriaDataSet<T> extends AbstractDataSet<T> {

 private Integer size;

 private ListDataSet<T> listDs;

 private final Provider<Criteria> criteriaProvider;

 public HibernateCriteriaDataSet(Provider<Criteria> criteriaProvider) {
  if (criteriaProvider == null) {
   throw new IllegalArgumentException();
  }
  this.criteriaProvider = criteriaProvider;
 }

 @SuppressWarnings("unchecked")
 public List<T> list(int firstResult, int maxResults) {
  if (listDs != null) {
   return listDs.list(firstResult, maxResults);
  } else {
   final Criteria crit = criteriaProvider.get();
   if (firstResult < 0 || maxResults <= 0) {
    final List<T> list = crit.list();
    size = list.size();
    listDs = new ListDataSet<T>(list);
    return list;
   } else {
    crit.setMaxResults(maxResults);
    crit.setFirstResult(firstResult);
    return crit.list();
   }
  }
 }

 public List<T> listAll() {
  return list(-1, -1);
 }

 public int size() {
  if (size == null) {
   final Criteria crit = criteriaProvider.get();
   crit.setProjection(Projections.rowCount());
   size = (Integer) crit.uniqueResult();
  }
  return size;
 }
}

Claro que nem todas consultas você vai conseguir fazer usando isso, então encare mais como um utilitário. Segundo é, para esses DataSet que acessam banco de dados, óbvio que precisa ser executado em um contexto transacional.

Para comprovar que funciona, criei uma tabela cargo com id e nome, e inseri alguns registros. Buscando todos registros através de um DataSet, fazendo um for each mandando meu iterator ler paginado de 5 em 5 (configuravel dentro de AbstractDataSet) e exibindo por primeiro o número de registros da consulta. Perceba que a cada 5 registros lidos um novo sql é executado para buscar os próximos (claro que 5 é um número muito pequeno para paginar, aqui estou usando só como exemplo, edite AbstractDataSet de acordo com suas necessidades ou até mesmo deixe isso configuravel nas sub classes):

DataSet<Cargo> ds = Cargo.findAll();
  System.out.println(ds.size());
  for (Cargo c : ds) {
   System.out.println(c.getName());
  }
Console:
Hibernate: select count(*) as col_0_0_ from cargo cargo0_
332
Hibernate: select cargo0_.cd_cargo as cd1_0_, cargo0_.nm_cargo as nm2_0_ from cargo cargo0_ limit ?
Account Manager
Administrador(a)
Administrador(a) Banco de Dados
Administrador(a) Empresas
Administrador(a) Materiais
Hibernate: select cargo0_.cd_cargo as cd1_0_, cargo0_.nm_cargo as nm2_0_ from cargo cargo0_ limit ?, ?
Administrador(a) Rede
Advogado(a)
Agente Administrativo
Agente Comercial
Agente Compras
Hibernate: select cargo0_.cd_cargo as cd1_0_, cargo0_.nm_cargo as nm2_0_ from cargo cargo0_ limit ?, ?
Agente Financeiro
Agente Negócios
...

Segue o que seria a implementação do método findAll. Esse primeiro usando o HibernateCriteriaDataSet:

final Session session = //obtem o Session, seja via Spring, ou como vc quiser
   final Provider<Criteria> crit = new Provider<Criteria>() {
    public Criteria get() {
     return session.createCriteria(Cargo.class);
    }
   };
   return new HibernateCriteriaDataSet<Cargo>(crit);
E esse segundo usando JpaQueryDataSet:
final EntityManager entityManager =  //obtem o EntityManager, seja via Spring, ou como vc quiser
   final String sql = "select e from " + Cargo.class.getName() + " e";
   final String countSql = "select count(*) from "
     + Cargo.class.getName() + " e";
   final Provider<Query> queryProvider = new Provider<Query>() {
    public Query get() {
     return entityManager.createQuery(sql);
    }
   };
   final Provider<Query> countQueryProvider = new Provider<Query>() {
    public Query get() {
     return entityManager.createQuery(countSql);
    }
   };
   return new JpaQueryDataSet<Cargo>(queryProvider, countQueryProvider);

Bem, eu não postei, mas já fiz implementações dessas que acessam objetos em xml e até mesmo um DataSet que lê paginado de outros DataSet's (por exemplo fazer um for each que lê de um banco de dados e de um arquivo xml de forma transparente e paginada). Pode ser que exista soluções muito melhores, mas na época não tinha achado nada que fizesse o que eu queria, por isso criei esse jeito, o que vem me quebrando um bom galho ultimamente.