Quase toda aplicação J2EE acessa dados de um banco de dados relacional. Por este motivo, o JDBC é provavelmente uma das APIs mais populares disponíveis para a plataforma Java. O JDBC é conceitualmente simples e fácil de usar, mas para aplicações em produção, vários detalhes podem tornar o desenvolvimento do create/read/update/delete (CRUD) mais trivial em uma tarefa difícil. Embora muitos frameworks, ferramentas e APIs possam simplificar o desenvolvimento, estas são freqüentemente muito complexas. Como uma alternativa, este artigo apresenta um pequeno conjunto de classes fáceis de usar, manter e estender.

Você pode estar pensando: mais uma ferramenta/assistente/API/framework para complicar nossa vida? Posso entender sua hesitação: o problema de mapeamento objeto-relacional está sendo abordado faz anos. Todo mundo tem uma solução que é mais fácil, melhor, mais rápida e mais barata. Para que precisaríamos de mais código que provavelmente faz o que alguém já fez? Por duas razões: a maioria das soluções são geralmente mais complicadas do que o necessário e também demandam um compromisso significativo: uma vez que entramos, é difícil cair fora.

Gosto de coisas simples. Um colega citou recentemente uma frase interessante: "A simplicidade conduz à onipresença, a complexidade conduz a obscuridade". E todos têm ouvido a citação de Einstein: "Uma teoria científica deveria ser tão simples quanto possível, mas não a mais simples".

E esta é uma citação do James Gosling: "A complexidade é de muitas formas perniciosa. A complexidade torna as coisas mais difíceis de entender, mais difíceis de construir, mais difíceis de depurar, mais difíceis de evoluir, mais difícil de fazer tudo”.

Poderia continuar. Há muitos exemplos de complexidade, muitos. No caso de acesso a banco de dados, podemos utilizar entity beans, uma ferramenta de mapeamento objeto-relacional ou o mais recente framework open source. Não tenho reclamações formais contra quaisquer uma dessas ferramentas; tenho certeza de que os autores tiveram uma grande visão ao desenvolvê-las. Para mim, hoje, quero algo bem simples.

Voltemos um pouco no tempo. A maioria das vezes, uma aplicação precisa obter informação de um usuário e as criar/editar/deletar/visualizar como linhas em um banco de dados. Um detalhe importante é que a informação que vai e provém do usuário deve estar na forma de strings. Isto significa que a validação deve ser feita na entrada e a formatação na saída. Isto não é difícil, mas é repetitivo e envolve numerosas verificações desnecessárias. Outra característica importante que os usuários esperam ter é a habilidade para obter um conjunto grande de resultados ou pular para o fim de um conjunto de registros, nada fácil de obter com um ResultSet.

Uma solução popular é a de manualmente ou automaticamente mapear o SQL e esquemas resultantes de tabelas para objetos e classes. No início, isto parece razoável, se não natural. Em geral, modeladores de banco de dados e modeladores de objeto tentam realizar a mesma coisa: gerenciar a informação empresarial de uma maneira lógica e extensível.

Infelizmente, a lua de mel termina por aqui. Modeladores de objetos preferem se isolar dos vários detalhes quanto a como os dados são inseridos e recuperados. Para alguns, o banco de dados é como um grande hashmap com mais de uma chave. Esta poderia ser uma avaliação precisa, mas há a necessidade de uso de SQL conhecido como table join. Peritos de banco de dados argumentam que junções dão poder aos bancos de dados relacionais. Modeladores de objeto poderiam encará-los como algo que só causa mais dificuldades do que benefícios. Apesar das suas opiniões, objetos e bancos de dados provavelmente não irão mudar significativamente no futuro próximo. Aplicações e lógica de negócio continuarão usando objetos, e persistência de dados continuará tendo a forma de um banco de dados relacional.

Barreiras culturais aparte, existem diferenças significativas entre classes/objetos e linhas/tabelas. Objetos podem incluir comportamento, o que os distingue de estruturas e outros tipos de agregados. Classes oferecem herança e polimorfismo, o que as torna reutilizáveis e extensíveis. Mas bancos de dados têm a ver com armazenamento eficiente e procura rápida. Isso não pode ser subestimado quando são consideradas dezenas de milhares, se não milhões, de linhas de informação potenciais. Esta informação e o seu acesso de modo eficiente provam o quão importante é o código que irá fazer sua manipulação.

Outro detalhe importante é que classes são fáceis de mudar e o código é freqüentemente refatorado quando for conveniente e útil. Mudar a estrutura de um banco de dados representa um problema muito maior. Mudar nomes de colunas ou mesmo criar novas tabelas e recarregar dados não é em si mesmo um desafio, mas ajustar todo o código que suporta esta estrutura pode ser bem problemático. Por isso, tipicamente, tabelas de banco de dados mudam pouco depois que foram povoadas.

Todas estas diferenças conduziram à chamada impedância de casamento, um termo relativo ao princípio de máxima potência de transferência emprestado da engenharia elétrica. Embora possamos extrair e inserir dados fácil e eficazmente em um banco de dados, os objetos que usam os dados poderiam não se ajustar bem. O resultado típico é a ocorrência de muitos códigos que se assemelham à serialização manual de objetos. Isto é tedioso e propenso a erros.

Para evitar o tédio, muitas ferramentas evoluíram no decorrer dos anos. A especificação J2EE oferece um modelo na forma de container-managed entity beans e a EJB-QL (Enterprise JavaBean Query Language). Na comunidade open source, o Hibernate tem se tornado uma solução popular. Estas soluções dependem de uma linguagem de consulta como a SQL, mas não precisamente. O resultado é que poderíamos ter que ajustar as consultas para se ajustarem à ferramenta, em lugar de utilizar o que o banco de dados é capaz de fazer por si só.

Estas podem ser soluções efetivas para uma ampla gama de questões, mas ainda não satisfazem um padrão arbitrário de simplicidade. Dou ênfase à expressão arbitrário, porque muitos acharão que estas soluções satisfazem as exigências: é apenas uma diferença de opinião e nada mais.

Simplificando a vida do desenvolvedor

Muitos desenvolvedores com os quais trabalhei não são modeladores de objetos ou modeladores de banco de dados; representam uma terceira categoria de programadores. Eles se sentem igualmente confortáveis com objetos e com o SQL; são os desenvolvedores em PowerBuilder. O PowerBuilder tem suas qualidades boas e ruins, como qualquer produto, mas em minha opinião, tem uma jóia: a janela de dados. Em vez de mapear toda a tabela ou resultset potencial em uma classe diferente, o PowerBuilder tem uma única classe: a janela de dados. Simplesmente falando, a janela de dados equivale a um ResultSet atualizável. Para usá-la, simplesmente emita uma consulta, qualquer consulta, não importa o quão complexa seja. Qualquer resultado retornado poderá ser formatado facilmente, poderá ser ordenado e poderá ser navegado em qualquer ordem. Ainda mais, quaisquer dos dados devolvidos poderão ser modificados e ser submetidos ao banco de dados mediante uma chamada de método. A janela de dados também controla o gerenciamento de todas as chaves primárias e transações.

Simplificando a vida do desenvolvedor, versão 1

Estando já convencido de que a janela de dados pode ser uma solução efetiva, comecei a escrever algo semelhante em JDBC com Java. Obtive uma solução bastante completa, mas havia problemas. Um das vantagens da janela de dados é que ela administra automaticamente chaves primárias e tipos de dados de colunas. Isto reduz significativamente o trabalho do desenvolvedor. O problema com minha implementação era a disponibilidade de drivers de banco de dados de alta qualidade. A implementação depende pesadamente de resultsets de metadados. Estes metadados, como quaisquer outros, tais como Java Reflection e XML Schema, serão poderosos quando for preciso. O problema é que muitos drivers, até mesmo aqueles de alta qualidade, não retornam todos os metadados necessários. Nada na especificação do JDBC exige isto dos drivers. Os métodos existem, mas não trabalham necessariamente para todas as combinações de drivers de bancos de dados.

Pelo que eu sei, o PowerBuilder também tem problemas de metadados ao utilizar o ODBC e os drivers nativos. A solução é implementar drivers de banco de dados personalizados para cada banco de dados suportado para garantir funcionalidade robusta. Tolamente, desperdicei muito tempo investigando por que alguns drivers não proviam todos os metadados. A resposta é simples: há muitos bancos de dados, muitos drivers e algumas pessoas que os desenvolvem. Provavelmente, metadados perfeitos não estão no topo da lista de prioridades; não é provável que seja atingido cem por cento de suporte no futuro próximo.

Simplificando a vida do desenvolvedor, versão 2

Tentei descobrir as necessidades de metadados na esperança de suportar os drivers de banco de dados e bancos de dados mais populares. Isto provou ser melhor, mas menos conveniente que a implementação original, e alguma coisa poderia ainda ser omitida.

Concluí que talvez estivesse sendo muito preciosista. Como outros, tentei resolver o problema de forma ampla. Reduzi meu escopo para algo mais manipulável, algo simples. Decidi dar prioridade às facilidades de navegação, formatação, análise gramatical e validação. Abandonei a idéia de gerenciar chaves primárias e deixei isto para o desenvolvedor. Concluí que no fim as chaves primárias não são a parte mais difícil do gerenciamento.

O resultado final é uma solução escalável que tolera limitações de drivers de banco de dados disponíveis, mas ainda é relativamente fácil de usar.

Agora na prática

Chega de introdução, chegou a hora de ver a implementação e alguns exemplos. Veremos como o pacote pode ser usado para executar operações básicas select/insert/update/delete e também para formatar e validar dados. A Figura 1 apresenta o diagrama de classes da UML para as classes responsáveis por facilitar o uso do JDBC.

29-05-2007pic01.JPG

Figura 1. Diagrama UML das classes básicas.

A classe Database faz duas coisas: mantém um mapa de Formatter e implementa métodos para selecionar, inserir, atualizar e apagar linhas. Também há um método validate(), sobre o qual falarei brevemente. Formatter é uma classe abstrata que define três métodos: parse(), format() e validate(). Como mencionado antes, a visão de dados dos usuários está na forma de strings, assim os formatters são responsáveis por converter tipos de banco de dados de/para strings. Sempre é possível a inserção de dados inválidos, por exemplo, caracteres não numéricos em um campo numérico, data formatada incorretamente ou strings que não casam com um padrão. Para cada um dos dados de coluna esperados, existe uma subclasse Formatter.

Vejamos um exemplo simples de seleção de dados na Listagem 1.

Connection con = getConnection();
myDB = new Database(false);
myDB.loadDefaultFormatters(con, "roles");
String[] params = {"roles.id", "99"};
Results rs = myDB.select(con, "select id,name,role from roles where id = ?", params, 10);
System.out.println("Name is " + rs.getString(0, 1));
System.out.println("Password is " + rs.getString(0, "roles.role"));

Listagem 1. Exemplo de seleção de dados.

A primeira linha obtém uma conexão com o banco de dados. Assumi que existem meios para obter a conexão, a maioria das aplicações o faz. Normalmente, provém de uma fonte de dados predefinida ou de uma chamada simples a DriverManager. O construtor do banco de dados possui um argumento booleano que diz à instância para fechar automaticamente a conexão depois de uma operação. Como provavelmente você deve saber, fechar conexões é um processo crítico e propenso a erros, portanto isto pode ser automatizado caso assim o prefira. O método loadDefaultFormatters() é um modo prático de carregar o formatter apropriado para uma determinada tabela. Este depende do método getColumnClass() em ResultSetMetaData. Possivelmente, isto poderia não funcionar, mas em minha experiência, este método parece funcionar com todos os bancos de dados que testei, inclusive com o Access, SQL Server, MySQL, Oracle e DB2. Alternativamente, podemos configurar o formatter para colunas específicas por nome. Provavelmente você desejará fazer isto em produção, já que o formatter padrão não possui muitas restrições de validação.

Este exemplo é de um simples select. Perceba que o SQL não é nada além do que você iria prover ao PreparedStatement. Na realidade, no fundo é exatamente isto, permitindo a utilização de qualquer SQL executado pelo seu banco de dados. O argumento params é um array de Strings que contem pares de nome de coluna e valor. A ordem dos parâmetros equivale a um HashMap ordenado, porém mais fácil de criar e inicializar. O nome das colunas é necessário, pois desta maneira a análise gramatical dos valores dos parâmetros poderá ser realizada corretamente.

O método select() retorna um objeto Results que equivale a um ResultSet, mas formata os resultados baseado em nomes de colunas. Se o nome de coluna não estiver disponível, tentará utilizar o tipo da coluna. Valores para uma determinada linha ou coluna podem ser recuperados em qualquer ordem. Perceba que o nome de coluna ou o numero da coluna pode ser especificado. O método select() levantará uma SQLException se algo der errado.

Agora analisaremos um exemplo de inserção na Listagem 2.

String[] values = {
   "roles.id", "99",
   "roles.name", "ahab",
   "roles.role", "captain"
};
String[] errors = myDB.validate(values);
if (errors != null) {
   for (int i=0; i      System.out.println(values[2*i] + ": " + errors[i]);
   }
} else {
   myDB.insertRow(con, values);
}

Listagem 2. Exemplo de inserção.

Este exemplo mostra como a validação pode ser usada. A classe Database possui um método validate() que aceita um array de valores a ser validado e retorna mensagens de erro caso necessário. Cada valor é verificado contra seu formatter correspondente. Se qualquer valor não for válido, o array conterá as mensagens de erro correspondentes. Valores válidos contêm um string vazio (não-nulo). Um valor de retorno nulo indica que não houve erros de validação.

O método insertRow() aceita uma conexão e os valores a inserir. Os valores são formatados como um array de Strings que contêm pares de nome de coluna e valor. Um SQLException será levantado se algo der errado.

Agora analisaremos um exemplo de atualização na Listagem 3.

String[] values = {
   "roles.id", "99",
   "roles.name", "ahab",
   "roles.role", "fool"
};
String[] params = {"roles.id", "99"};
int n = myDB.updateRows(con, values, "id=?", params);

Listagem 3. Exemplo de atualização.

O terceiro parâmetro na última linha da Listagem 3 representa os critérios para uma cláusula opcional WHERE. Este parâmetro pode ser nulo, e neste caso, todas as linhas serão atualizadas.

E finalmente, analisemos um exemplo de delete na Listagem 4. Perceba que a validação pode (e deve) ser feita também com parâmetros.

String[] params = {"roles.id", "99"};
String[] errors = myDB.validate(params);
if (errors != null) {
   for (int i=0; i      System.out.println(values[2*i] + ": " + errors[i]);
   }
} else {
   int n = myDB.deleteRows(con, "roles", "id=?", params);
}

Listagem 4. Exemplo de remoção.

Minha meta não foi substituir qualquer framework de banco de dados. Foi simplesmente um esforço para resolver um conjunto de problemas com pouco código. É possível utilizar estas classes como base para um framework mais completo, que faz cache de dados e se integra diretamente com JSP/servlets. Não seria difícil de construir taglibs personalizadas ou componentes para outros frameworks web.

Claro que esta solução não é perfeita. Configurar formatters para cada coluna de cada tabela que você planeja acessar pode ser muito trabalhoso. Felizmente, só precisamos fazer isto apenas uma vez. Idealmente, todas as informações deveriam ser definidas quando a tabela fosse definida, mas isso normalmente não acontece. As informações poderiam ser carregadas de um arquivo XML ou poderiam ser armazenadas no próprio banco de dados; isso é o que PowerBuilder faz.

É bastante simples? Você decide. É possível que seja simples demais; alguns poderiam argumentar que não é coisa para o horário nobre. Isso está certo. Em minha opinião, é mais fácil acrescentar funcionalidade a uma solução simples em lugar de simplificar uma solução complexa.