Páginas

quinta-feira, 3 de janeiro de 2013

Um pequeno framework para JDBC




Nesse artigo irei apresentar um pequeno framework que irá adicionar funcionalidades extras ao JDBC, mais especificamente ao tradicional PreparedStatement do pacote java.sql. As funcionalidades extras são:

1- Conversão automática de um ResultSet para uma lista de objetos de negócio, eliminando assim a necessidade de manipular diretamente um ResultSet.

2- Cache opcional para buscas frequentes.

Vamos ver um exemplo de uso desse framework:

Tabela


CREATE TABLE `usuario` (
   `cod_usuario` int(11) NOT NULL AUTO_INCREMENT,
   `nome_usuario` text NOT NULL,
   `email_usuario` text NOT NULL,
   PRIMARY KEY (`id`)
)ENGINE=InnoDB;


Classe do dominio do negócio:


class Usuario{
private int id;
private String nome;
private String email;
//getters e setters
}

Dao usando o framework

Conversão


//dao
public List<Usuario> buscarPeloNome(java.sql.Connection con, String busca){
return new PreparedStatementPlus(con)
.prepareStatement("select cod_usuario as id from usuarios where nome_usuario = ?")
.setString(1, busca)
.executeQuery(Usuario.class);
}

Chemei ele de PreparedStatementPlus, devido as funções extras.

Onde está o resultSet? Vamos entender o código acima: O método buscarPeloNome() retorna uma lista de objetos Usuario. O método executeQuery() de um PreparedStatement tradicional iria retornar um ResultSet, mas o método executeQuery() do PreparedStatementPlus aceita um argumento informando o tipo de classe de retorno. Assim esse método, ao invés de retornar um ResultSet para que nós manualmente criemos os objetos, retorna logo a lista de objetos desejada. O mapeamento objeto-relacional é feito via reflection, o nome da coluna deve ser igual ao nome da propriedade da classe para que seja feita a atribuição. No caso do nome da coluna ser diferente, pode-se definir um apelido (ex. select cod_usuario as id from usuarios) para que os nomes casem.

Cache


Cada instancia do PreparedStatementPlus tem um cache interno para evitar consultas repetidas. Por exemplo:

PreparedStatementPluss p = new PreparedStatementPlus(con);//autoComit é setado para false a fim de se obter a proteção do banco quanto ao ACID
for(int i = 0; i < 1000; i++){
p.prepareStatement("select * from usuarios")
       
        //A LINHA ABAIXO CONSULTA O BANCO SÓ UMA VEZ, DURANTE O PRIMEIRO LOOP
        //NOS OUTROS LOOPS O FRAMEWORK DETECTARÁ QUE SE TRATA DA MESMA      
        //CONSULTA E IRÁ BUSCAR O RESULTADO NO CACHE
int total = p.executeQueryAndCacheResult(Usuario.class).size();

System.out.println(total+" usuarios encontrados");
}

Como há multiplas consultas para a mesma instancia do preparedStatementPlus este se utiliza de cache para consultas repetidas. Mas o seguinte codigo não guarda em cache

for(int i = 0; i < 1000; i++){
        //para cada loop um novo preparedStatementPlus é criado, 
        //inviabilizando assim o cache automático
PreparedStatementPluss p = new PreparedStatementPlus(con);
p.prepareStatement("select * from usuarios")
int total = p.executeQueryAndCacheResult(Usuario.class).size();
System.out.println(total+" usuarios encontrados");
}

Pode-se também transformar o cache de primeiro nível do PreparedStatementPlus em cache de segundo nível por guardá-lo em algum lugar. Ex. Guardando o cache em uma sessão JEE

//Servlet
PreparedStatementPluss p = new PreparedStatementPlus(con);
p.prepareStatement("select * from usuarios")
List<Usuario> usuarios = p.executeQueryAndCacheResult(Usuario.class);
//os usuarios são guardados em cache na sessão 
request.getSession().setAttribute("cache", p.getCache());

//em um outro servlet
PreparedStatementPluss p = new PreparedStatementPlus(con);

//faz o PreparedStatementPlus ler o cache informado
p.setCache((java.util.Map)request.getSession().getAttribute("cache"));
p.prepareStatement("select * from usuarios")

//não vai ao banco, pois esta consulta já esta no cache
List<Usuario> usuarios = p.executeQueryAndCacheResult(Usuario.class);


Naturalmente para este tipo de cache vocês mesmos deverão cuidar do ACID no banco, por exemplo ao inserir, editar ou deleter um usuario chame request.getSession().setAttribute("cache", null); para invalidar o cache, pois perdeu a sincronia com o banco

Deploy

O preparedStatement precisa do commons-beanutils.jar no classpath para funcionar

Código

Segue abaixo o código completo da classe:



import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.beanutils.BeanUtils;

/**
 * Serviços: 
 * Conversão de resultado para List de objeto
 * Cache de consultas
 * @author Gustavo
 */
public class PreparedStatementPlus {
    private String sqlquery;
    private StringBuilder parametrosString = new StringBuilder();
    private PreparedStatement p;
    private Connection con;
    
    /*
     * Cache de primeiro nível e/ou segundo 
     */
    private Map cache;

    public PreparedStatementPlus(Connection con) throws Exception{
        con.setAutoCommit(false);
        this.con = con;
    }

    /*
     * Método genérico, retorno lista de objetos encontrados na consulta.
     * As tuplas encontradas são convertidas no objeto do tipo clazz, onde nome da coluna (redefinida ou não com AS) = nome da propriedade
     */
    public <T> List<T> executeQuery(Class<T> clazz) throws Exception{
        con.setAutoCommit(false);
        List<T> retorno = new ArrayList<T>();
        //verifica se tem cahce para esta consulta
        if(getCache() == null){
            setCache(Collections.synchronizedMap(new HashMap()));
        }
        if(getCache().containsKey(indiceDoCache())){            
            //tem no cache, retorna a lista já montada
            retorno = (List<T>)getCache().get(indiceDoCache());
        }else{            
            //não tem cache executa ela
            ResultSet rs = p.executeQuery();
            while(rs.next()){
                Object objeto = clazz.newInstance();
                ResultSetMetaData meta = rs.getMetaData();
                for(int i = 1; i <= meta.getColumnCount(); i++){
                    String coluna = meta.getColumnLabel(i);
                    //define via reflection a propriedade do objeto que tem o mesmo nome da coluna
                    try{
                        //System.out.println("propriedade: "+coluna+", valor: "+rs.getObject(coluna));
                        BeanUtils.setProperty(objeto, coluna, rs.getObject(coluna));
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                }
                retorno.add((T)objeto);
            }
            rs.close();
        }
        p.close();
        return retorno;
    }

   /*Guarda o resultado em cache*/
    public <T> List<T>  executeQueryAndCacheResult(Class<T> clazz) throws Exception{
        con.setAutoCommit(false);
        List<T> retorno = executeQuery(clazz);
        if(getCache() == null){
            setCache(Collections.synchronizedMap(new HashMap()));
        }
        //coloca no cache o resultado, se ainda não adicionado
        if(!cache.containsKey(indiceDoCache())){
            //ainda não está no cache, adiciona
            getCache().put(indiceDoCache(), retorno);
        }
        return retorno;
    }
    
    public PreparedStatementPlus prepareStatement(String sql)throws Exception{
        this.sqlquery = sql;
        parametrosString = new StringBuilder();
        p = con.prepareStatement(sqlquery);
        return this;
    }

    public int executeUpdate() throws Exception{
        int retorno =  p.executeUpdate();
        //sem retorno, pode fechar o preparedStatement
        p.close();
        return retorno;
    }


    //***SETAGEM DOS PARÃMETROS****//
    public PreparedStatementPlus setString(int parametro, String valor) throws Exception{
        p.setString(parametro, valor);
        parametrosString.append(" ?").append(parametro).append(":").append(valor).append("? ");
        return this;
    }

    public PreparedStatementPlus setInt(int parametro, int valor) throws Exception{
        p.setInt(parametro, valor);
        parametrosString.append(" ?").append(parametro).append(":").append(valor).append("? ");
        return this;
    }

    public PreparedStatementPlus setLong(int parametro, long valor) throws Exception{
        p.setLong(parametro, valor);
        parametrosString.append(" ?").append(parametro).append(":").append(valor).append("? ");
        return this;
    }

    public PreparedStatementPlus setDate(int parametro, java.util.Date valor) throws Exception{
        p.setDate(parametro, new java.sql.Date(valor.getTime()));
        parametrosString.append(" ?").append(parametro).append(":").append(valor).append("? ");
        return this;
    }

    /*
     * Formato dd/MM/yyyy
     */
    public PreparedStatementPlus setDate(int parametro, String valor) throws Exception{
        java.util.Date data = new SimpleDateFormat("dd/MM/yyyy").parse(valor);
        p.setDate(parametro, new java.sql.Date(data.getTime()));
        parametrosString.append(" ?").append(parametro).append(":").append(valor).append("? ");
        return this;
    }

    public PreparedStatementPlus setDate(int parametro, java.sql.Date valor) throws Exception{
        p.setDate(parametro, valor);
        parametrosString.append(" ?").append(parametro).append(":").append(valor).append("? ");
        return this;
    }

    public PreparedStatementPlus setTimestamp(int parametro, java.sql.Timestamp valor) throws Exception{
        p.setTimestamp(parametro, valor);
        parametrosString.append(" ?").append(parametro).append(":").append(valor).append("? ");
        return this;
    }

    public PreparedStatementPlus setBoolean(int parametro, boolean valor) throws Exception{
        p.setBoolean(parametro, valor);
        parametrosString.append(" ?").append(parametro).append(":").append(valor).append("? ");
        return this;
    }
    //***FIM DA SETAGEM DOS PARAMETROS***//


    /*
     * O indice é o sqlqueri + parametros
     */
    private String indiceDoCache()throws Exception{
        return sqlquery+parametrosString.toString();
    }

    /**
     * @return the cache. Recupera cache atualizado
     */
    public Map getCache() {
        return cache;
    }

    /**
     * @param cache the cache to set. Seta um cache de fora, segundo nivel armazenado em algum lugar
     */
    public void setCache(Map cache) {
        this.cache = cache;
    }

    public PreparedStatementPlus reset(){
        sqlquery = null;
        parametrosString = new StringBuilder();
        try{p.close();}catch(Exception e){}
        return this;
    }

}


Certamente é preferível o uso de frameworks já consolidados, hibernate, padrão JPA etc. Não é o foco competir com esses frameworks já consagrados. No entanto em casos onde não se pode implantar esses frameworks podemos otimizar os recursos nativos do JDBC para melhorar a produtividade e performance.

Atenciosamente,
Gustavo Marques



Nenhum comentário:

Postar um comentário

Veja também

Related Posts Plugin for WordPress, Blogger...