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