A plataforma Java estreou com os applets, mas começou a ser levada a sério a partir da introdução da JDBC. Começamos com uma API de baixo nível, modelada segundo o padrão ODBC da Microsoft, que é eficiente, mas requer bastante codificação. Com o tempo surgiram soluções de persistência cada vez mais fáceis e transparentes, como o Persistent State Service (PSS) da OMG, Java Data Objects (JDO; JSR-12), Hibernate, EJB (BMP e depois CMP), Castor e dezenas de alternativas proprietárias.

Mas com o tempo também descobrimos que as soluções de alto nível, orientadas à produtividade e a técnicas OO, não servem para todos os cenários possíveis. Da mesma forma que já virou rotina se falar que nem todas as aplicações Java precisam de EJB ou de servidores J2EE ou de web services, também acontece em alguns casos de a velha API JDBC ser a melhor solução para persistência. Ou mesmo a única viável.

Este artigo explora algumas idéias de programação de bancos de dados em Java, pensando no programador que por algum motivo não quer, ou não pode, utilizar persistência automática. Não vamos nos dedicar especificamente à API JDBC – pressupomos alguma familiaridade com conexões, statements, resultsets e companhia. O plano é explorar técnicas que permitem programar diretamente em JDBC, mas com vantagens de estruturação, desempenho e produtividade. E se o seu problema é decidir entre o uso de JDBC ou de uma camada de persistência de mais alto nível, também exploramos essa questão, apresentando os pontos fortes e fracos de cada alternativa.

De JDBC a DAO

Quando a JDBC foi lançada, as primeiras aplicações feitas com a API tinham uma tendência de ser bastante desestruturadas, com código JDBC espalhado por toda a aplicação, misturado com a lógica de negócio e de GUI. A situação era bem pior do que em ambientes de desenvolvimento cliente/servidor então existentes, como o PowerBuilder ou o VisualBasic, porque Java não tinha componentes de GUI “data-aware” ou IDEs especializados em programação de bancos de dados. Com o tempo a tecnologia Java evoluiu, mas em direções diferentes dos ambientes RAD[1]. Por um lado, surgiram várias soluções de persistência objeto/relacional (O/R), mas em paralelo, houve também uma evolução das práticas de programação de BD “hardcore”, com código JDBC escrito à mão.

A mais importante evolução a partir da programação JDBC pura foi o padrão de projeto DAO (Data Access Object). A motivação deste padrão é tornar o código de persistência mais organizado, reusável e desacoplado da lógica de negócio. Temos duas entidades principais: os objetos de valor (Value Objects ou VOs) e as classes DAO. Veja um exemplo na Listagem 1.

A prática mais comum é ter uma classe de VO para cada tabela, com cada coluna mapeada para um atributo, e cada linha mapeada para uma instância do VO. Mas também podemos ter VOs definidos a partir de classes de um modelo orientado a objetos; esse modelo será mapeado para tabelas não necessariamente idênticas, um para um, às classes (por exemplo, devido à normalização). Ou podemos ter uma mistura de ambos os critérios. Em qualquer caso, o VO é um “pacote” de informações que serão preservadas no banco de dados.

O código que faz a persistência fica na classe DAO, a qual muitas vezes é implementada como uma classe utilitária sem nenhum estado – com métodos static ou seguindo o pattern Singleton. Neste artigo optamos por DAOs “stateful” (com estado) e com instâncias diferenciadas, ou seja, não-Singleton. Como podemos ver na Listagem 1, isso é útil para associar o DAO a conexões, de forma a suportar a execução de várias operações persistentes numa mesma transação – sem precisar passar a conexão como parâmetro a cada método.

DAOs e persistência objeto/relacional

Os mandamentos da programação orientada a objetos nos fariam associar a funcionalidade aos dados, por exemplo escrever ordem.update() ao invés de dao.update(ordem). Mas o desacoplamento promovido pelo DAO serve para tornar o VO independente do mecanismo de persistência. Se a implementação inicial for feita com JDBC, mas quisermos migrar para JDO na próxima versão, o encapsulamento do código de persistência nos DAOs permite fazer essa mudança com um impacto mínimo sobre o código dos VOs ou de qualquer outra parte da aplicação.

Os sistemas de persistência automática para Java mais festejados são precisamente aqueles que se distinguem por suportar persistência sobre objetos de valor, também chamados de POJOs (“Plain Old Java Objects”), em contraste a outros sistemas como o EJB/CMP (até 2.1) que exigem a implementação de uma série de interfaces para tornar um objeto “persistível”. Nestes sistemas, como o Hibernate ou o futuro EJB/CMP 3.0, a implementação do DAO é generalizada pelo framework. Em vez de termos que escrever métodos para restore, create etc., o sistema de persistência é capaz de executar estas operações para qualquer POJO. Isso é feito, em primeiro lugar, com o uso de reflection, instrumentação de bytecode (que o JDO chama de “enhancing”), ou ambos – de maneira que o sistema de persistência possa ler os atributos do POJO para fazer um update no banco de dados ou definir esses atributos após uma leitura.

[nota] Neste artigo usamos “update” em minúsculas para indicar uma modificação no banco, com os comandos SQL UPDATE, INSERT ou DELETE, ou via métodos que os executem. [/nota]

Em segundo lugar, com a geração automática de código SQL para as operações de leitura, update etc. de cada POJO. Ambas as funcionalidades costumam depender também de arquivos de metadados ou descritores, que especificam o mapeamento entre tabelas e POJOs.

Os sistemas de persistência automática têm outras vantagens sobre a programação JDBC:

  • Há muito menos código para escrever (obrigatoriamente, só os POJOs).
  • Podemos contar com um cache de memória dos POJOs, que elimina execuções repetitivas de consultas que retornam os mesmos objetos.
  • Podemos ter facilidades de locking automático, otimista ou pessimista.
  • O gerador automático de consultas pode gerar código otimizado para cada SGBD.
  • Recursos importantes de orientação a objetos são mapeados para o mundo relacional, por exemplo usando tabelas diferentes para cada camada de herança e gerando joins para ler os objetos.

Estas são qualidades comuns a todos os sistemas de persistência objeto/relacional, não havendo muita diferença pelo fato de se usar POJOs ou não. Por outro lado, também existem algumas desvantagens comuns a todos os sistemas O/R:

  • Qualquer consulta que não seja trivial e que não seja mapeável diretamente para um modelo orientado a objetos não será suportada pelo sistema de persistência.
  • Mesmo para as operações que são facilmente mapeáveis para um modelo OO, podemos ser obrigados a usar consultas ad hoc por motivo de desempenho.

Por exemplo, uma consulta que retorna o número médio de vendas diárias por dia por item, em todo o ano de 2004, é extremamente eficiente em SQL:


SELECT codItem, TRUNC(data, 'yyyymmdd’), AVG(quantidade) FROM VENDA

  WHERE data >= ? AND data < ?   --- Passar 01/01/2004 e 01/01/2005

  GROUP BY codItem, TRUNC(data, ‘yyyymmdd’);

A princípio, esta consulta poderia ser substituída por código puramente OO. Podemos invocar um método do sistema O/R que retorna todo o extent (conjunto total de instâncias) de Venda na forma de uma Collection de POJOs. Depois filtramos essa coleção em memória, eliminando as vendas que não sejam para o ano 2004. Finalmente, percorremos os itens restantes, fazendo os agrupamentos e calculando as médias. Mas um desenvolvedor experiente saberá que isso terá um desempenho terrível. Os proponentes das ferramentas O/R afirmarão que não, pois existe um cache em memória dos objetos Venda e após algumas consultas estes objetos tendem a estar imediatamente disponíveis.

Isso realmente funciona muito bem para tabelas pequenas. Mas e se nossa tabela de vendas tiver um milhão de registros para cada ano? Uma resposta possível é "memória é barata, compre mais" – mas isso não produz o desempenho desejado. Digamos que você pode ser dar ao luxo de usar um heap de 10 Gb, acomodando todos os registros no cache de POJOs. Se o fizer, terá outros problemas de desempenho. Para detalhes, veja o quadro “Os problemas de caches gigantes”.

A solução seria usar APIs de consulta mais completas? Ao invés de somente retornar o extent, o sistema O/R poderia também suportar opções de filtros, agrupamento e outras. Mas nesse caso, é necessário suportar praticamente todas as facilidades do SQL, o que equivale a consultas ad hoc – mas com uma sintaxe muito inferior. Precisamos criar um objeto de consulta, definir vários parâmetros para o filtro, ordenação, agrupamento etc. E o resultado será menos legível que uma versão SQL. Como concluímos no artigo “Programação com Regras” (Edição 15), linguagens especializadas existem por boas razões.

De fato, raros sistemas O/R se dão ao trabalho de suportar mais que um mínimo de opções de consultas através de APIs. O que fazem é oferecer facilidades de consultas ad hoc. Estas facilidades têm a pretensão de evitar a poluição de programas “OO-puro” com código SQL, através do uso de linguagens de consulta especializadas. Todavia, estas linguagens costumam consistir em 90% de SQL e 10% de extensões OO. Algumas extensões como as necessárias para mapear herança para joins são extremamente úteis. Por outro lado, acho difícil justificar a substituição de uma sintaxe SQL funcionalmente satisfatória, como “TO_UPPER(x)”, por algo como “x.toLowerCase()” (como é feito no JDO).

É importante observar que essas linguagens de consultas objeto/relacionais são altamente incompatíveis entre diferentes padrões de sistemas O/R (como JDO versus Hibernate), enquanto o padrão ANSI SQL-92 é bem mais portável em comparação. E em alguns casos a emenda é pior que o soneto: se fôssemos seguir à risca o paradigma de linguagens OO, uma expressão como “x.toLowerCase()”, quando x=NULL, deveria gerar um erro (como NullPointerException). Mas não é o que acontece, pois as linguagens de “consulta O/R” são mapeadas para equivalentes em SQL e executam com a semântica do SQL, neste caso temos o mapeamento TO_UPPER(NULL)?NULL.

De qualquer forma, com consultas ad hoc passamos por cima do sistema de persistência, programando quase do mesmo jeito que faríamos numa aplicação que só usasse JDBC. Poupamos somente a escrita do “esqueleto” do código de consulta (ex.: criar um Statement, fazer um loop para ler o ResultSet etc.). Isso é especialmente verdadeiro para consultas que não retornam POJOs mas sim colunas “brutas”, como no exemplo com o agrupamento sobre a tabela de vendas.

Quando estudamos o assunto de linguagens orientadas a objetos versus bancos de dados relacionais, logo somos informados de um “gap semântico”: nossa linguagem e nosso BD seguem paradigmas diferentes, nem sempre compatíveis. Registros não são objetos; chaves estrangeiras não são relacionamentos entre objetos; herança não existe no modelo relacional; normalização não faz parte de linguagens OO; e por aí vai.

Depois que aprendemos isso, somos apresentados a soluções O/R (que vão de ferramentas de persistência automática até bancos OO-puros), visando eliminar o gap semântico através do mapeamento automático de conceitos diferentes entre os dois paradigmas. Por exemplo, relacionamentos são mapeados para chaves estrangeiras e tabelas associativas; herança pode ser mapeada para várias tabelas através de joins. Qual é o furo dessa promessa das ferramentas O/R? É simples: funciona bem, mas só para consultas simples. Tão logo sua aplicação precise fazer consultas complexas – com agrupamentos, joins entre várias tabelas, outer joins, consultas aninhadas, invocações a stored procedures, uso de funções SQL avançadas (como as de OLAP), funções proprietárias (como partições ou tabelas aninhadas) – o gap semântico torna-se intratável, e as ferramentas O/R jogam a toalha.

Isso tudo pode parecer pessimista, mas não significa que você deva abandonar as soluções O/R. Elas podem ser excelentes para implementar boa parte de uma aplicação, especialmente as operações CRUD (Create, Restore, Update, Delete) para o ciclo de vida básico dos dados. Mas são raras as aplicações não-triviais que só fazem manipulações de dados nessa categoria. E são abundantes os cenários em que precisamos de consultas ad hoc: na geração de relatórios; em operações ETL (Extract-Transform-Load), em data mining e OLAP. E mesmo no escopo de CRUD, as consultas ad hoc são necessárias em cenários mais pesados. Por exemplo, ao expurgar todas as vendas do ano anterior da tabela principal para uma tabela de histórico ou consolidada, você não vai querer carregar na memória um milhão de objetos de uma tabela, somente para regravá-los em outra.

DAOs envenenados

Já que não podemos nos livrar totalmente de consultas ad hoc e, por conseqüência, de APIs de baixo nível, vale a pena continuar investindo no conhecimento de programação JDBC.

Como o desempenho é uma preocupação constante, o problema mais sério a atacar é a falta de cache nas consultas feitas com JDBC. Isso é especialmente sério para os chamados “registros populares” (ou "objetos populares"), os que são utilizados com muita freqüência relativamente ao seu número ou à sua distribuição.

Por exemplo, numa tabela EMPRESA no topo da hierarquia de negócio da aplicação (empresa possui departamentos, que possuem funcionários etc.), com apenas um registro, este registro solitário será extremamente popular, mesmo que não seja utilizado com muita freqüência. Já numa tabela FUNCIONARIO com 10 mil registros, para uma aplicação de ERP que consulta esta tabela intensamente, todos os registros são populares. Fazer cache de registros nessas categorias (que poderíamos chamar “populares por número”) é bastante simples, sendo uma prática freqüente em aplicações que usam programação JDBC.

Parece relativamente simples implementar um cache manual como mostrado na Listagem 2, mas isso oculta algumas dificuldades e decisões importantes:

  • Consistência: Estamos supondo que, uma vez carregada uma Empresa para a memória, é aceitável retornar sempre o mesmo objeto do cache, sem nos preocuparmos com a consistência entre o cache e o banco de dados. Se as operações UPDATE, INSERT e DELETE desses objetos também são feitas somente através de EmpresaDao, isso parece seguro, pois os outros métodos de DAO podem manter o cache consistente, por exemplo removendo um objeto do cache após fazer um DELETE. Obviamente não podemos ter a possibilidade de aplicações externas ou consultas SQL ad hoc modificarem a tabela EMPRESA.
  • Resultados nulos: No método find(), cuidamos para não fazer cache de consultas mal sucedidas. Lembre que num Map, podemos mapear uma chave para um valor nulo. Portanto, poderíamos ter adotado a estratégia de fazer o cache.put() mesmo quando a consulta não encontra nenhum objeto, e consultar o cache da seguinte forma:
if (cache.contains(key)) return (Empresa) cache.get(key);

Isso seria mais eficiente, pois a partir do momento em que for feita uma consulta como find("XYZ"), sendo que não existe nenhuma empresa com nome="XYZ", o cache ficará com um mapeamento "XYZ"?null, e futuras invocações a find("XYZ") retornarão imediatamente a partir do cache, sem necessidade de fazer a consulta. Mas esse cache de consultas "nulas" oculta um perigo. Pode ocorrer um número arbitrário de consultas com chaves inexistentes – basta pensar em usuários humanos cometendo erros de digitação num formulário. Isso faria o uso de memória do cache crescer sem limite, especialmente se a aplicação ficar um longo período no ar, sem reinicialização. Por isso, em geral é melhor não fazer cache de consultas que não retornam dados, ainda que isso diminua um pouco a eficiência do cache. Mas esta é uma decisão caso a caso.

Transações: Se a instância de EmpresaDao for compartilhada por transações concorrentes, precisamos de sincronização para garantir que as operações sobre o cache sejam consistentes com o banco de dados. Não podemos permitir que diversas transações, em threads separados, executem operações como update() e restore() simultaneamente confiando somente no controle de concorrência do banco de dados, o qual não irá proteger o acesso às estruturas de dados do cache, nem garantir a atomicidade da operação completa (acesso ao cache + acesso ao BD). Uma estratégia de caching descuidada pode criar bugs de consistência entre o cache e banco de dados; pode chegar até a corromper o BD, causando a perda do resultado de transações inteiras.

Outro problema sério é como lidar com erros. Se uma operação UPDATE, INSERT ou DELETE falhar, não devemos refletir esta operação no cache. Isso pode parecer simples, pois se houver algum erro na operação no banco de dados (ex.: uma violação de constraint), uma SQLException será lançada ao executar o statement, antes de termos modificado o cache. O problema é que a invocação a esses métodos pode ser apenas uma das muitas operações de atualização feitas por uma transação – e um erro em qualquer update deverá cancelar os efeitos de todos os updates anteriores da mesma transação, inclusive os que tiveram sucesso. Somente após o commit() temos a garantia que uma atualização será durável, e só então poderíamos atualizar o cache de forma segura. Como resolver isso?

Uma possibilidade é utilizar auto-commit – ou fazer o commit() nos métodos do DAO – para operações de atualização. Isso é aceitável em transações que executem uma única atualização, mas não é escalável para aplicações menos triviais. Outra solução popular é limitar o uso de cache a objetos não atualizáveis, como objetos de configuração que são cadastrados estaticamente (na população inicial da base de dados), ou objetos cadastrados de forma especial, por exemplo via importação de sistemas externos; sendo que nessas ocasiões podemos ter um “gancho” para limpar o cache.

Uma terceira solução (muito mais divertida) é programar um cache mais sofisticado.

Caches de gente grande

A implementação de cache da Listagem 2 tem pouca utilidade, pois oferece um ganho de desempenho desprezível exceto para os raros casos em que é amplamente aplicável (como objetos imutáveis), e parece criar muitas dores de cabeça para valer a pena. Mas isso não nos deve fazer desistir da idéia de utilizar caches de memória para programas que usam JDBC. Afinal, sistemas O/R fazem isso de forma confiável e resultando num desempenho excelente. É preciso somente aprender alguns novos truques. Aliás, mesmo que você prefira usar persistência automática, é bom saber como essas tecnologias funcionam para utilizá-las de forma mais eficaz.

Na Listagem 3, temos duas camadas de cache. O cache global, que fica numa variável static, é compartilhado por toda a aplicação e tem um ciclo de vida de longo prazo. Na implementação apresentada, uma vez que um POJO entre nesse cache global, ele nunca será eliminado.

O cache local é específico para cada transação. Pressupomos um mapeamento um para um entre transações e threads; ou seja, se a aplicação possuir várias transações simultâneas, elas estarão executando em threads distintos. Isso nos permite utilizar facilidades da classe ThreadLocal, que funciona como um Map onde a chave (implícita) é o thread corrente, e o valor é qualquer objeto desejado – no caso um HashMap que é a estrutura de cache local.

A estratégia de uso do cache de duas camadas não é complexa, mas tem algumas sutilezas. Detalhamos a seguir a lógica de cada operação com este cache:

  1. A operação find() procura primeiro no cache local. Se o objeto for encontrado, ele é retornado.
    1. Se não encontramos o objeto no cache local, tentamos novamente, só que no cache global. Se o objeto for encontrado ele é retornado, mas também é adicionado ao cache local. Isso agiliza pesquisas futuras, pois consultas ao cache global são menos eficientes (por exigirem sincronização). E o cache local também tem outros papéis importantes, como logo veremos.
    2. Se o objeto não for encontrado em nenhum cache, ele é lido do banco de dados e registrado em ambos os caches, local e global.
  2. As operações insert() e update() registram o novo estado do objeto, mas só no cache local.
  3. A operação remove() também reflete esta ação no cache local, porém de uma forma diferente: substituindo o mapeamento {nome®Objeto} por {nome®null}.
  4. Temos um novo método close() que deve ser invocado quando fechamos uma transação. O argumento commit=true indica que a transação foi confirmada com sucesso: ou seja, as alterações de registros foram tornadas duráveis no SGBD, e devemos fazer o mesmo no cache. Mesmo se não tiver havido commit, é essencial invocar este método (veja os comentários a seguir). Note também que os mapeamentos {nome®null}, produzidos pelos remove(), eliminarão POJOs do cache global.

Essa estrutura de cache de duas camadas emula o comportamento das transações do SGBD. Num banco transacional, as atualizações feitas por uma transação só são visíveis por outras transações após o commit. O cache local funciona da mesma forma: à medida que criamos, alteramos ou removemos objetos, estas operações (além de executadas sobre a base de dados) são registradas apenas no cache local da transação. Somente após o commit tornamos estas alterações visíveis para outras transações, refletindo-as no cache global.

O cache global é importante porque evita consultas sucessivas aos mesmos objetos, entre transações sucessivas. Esse cache funciona como um espelho do banco de dados na memória, devendo ser totalmente consistente com o estado do BD, e devendo também suportar o isolamento entre transações. Uma conseqüência disso é que, na etapa 1.1 do algoritmo anterior, quando um objeto é encontrado no cache global, este objeto deve ser clonado. Se o clone não fosse feito, as alterações feitas ao objeto por uma transação ficariam visíveis para outras transações que tivessem obtido o mesmo objeto do cache global.

Outro papel importante do sistema de cache (especialmente o de cache local) é manter a integridade referencial dos POJOs. Isso significa que, para uma determinada transação, duas consultas a EmpresaDao.find(nome) devem retornar o mesmo objeto, para o mesmo valor nome.

EmpresaDao dao = new EmpresaDao(conn); Empresa e1 = dao.find("Sun Microsystems"); Empresa e2 = dao.find("Sun Microsystems"); // Testa integridade referencial e2.setCotacao(10.6); System.out.println(e1.getCotacao()); // deve imprimir “10.6” // Testa integridade de updates no BD e1.setEndereco("Santa Clara, CA"); dao.update(e1); // Deve gravar cotacao=13.2 e endereco=Santa Clara, CA dao.update(e2); // Redundante com o update anterior!

Neste exemplo, as duas consultas find() devem retornar o mesmo objeto. Se retornassem objetos distintos (e1 != e2), ainda que a princípio iguais (e1.equals(e2)), teríamos bugs de consistência. Alterações feitas em e1 não seriam vistas por e2 e vice-versa, pois do ponto de vista do heap de objetos do Java seriam objetos totalmente independentes. Também poderia haver inconsistência entre as operações de atualização do DAO. No exemplo, se as referências e1 e e2 não apontassem para o mesmo objeto em memória, e2.update() seria cancelado o efeito de e1.update(); e o registro no BD ficaria com o valor antigo para o endereço e o valor novo somente para a cotação.

Caches parciais

Já discutimos a necessidade de cache para objetos populares por número. Mas um caso mais complicado é a popularidade por distribuição. Imagine uma tabela ORDEM_SERVICO que tem 10 milhões de registros. Nem todos esses registros são "populares", pois 99% deles são ordens já fechadas, que permanecem na tabela apenas para finalidade de histórico – e só são usados eventualmente por algum relatório mensal ou anual. Mas todos os registros com status "ABERTO" são populares, pois são constantemente consultados e modificados durante o ciclo de vida de uma ordem de serviço, até o seu fechamento. Os registros que foram fechados muito recentemente também são populares, pois tendem a ser consultados por gerentes de operações, por funcionários que trabalham sobre a ordem de serviço, ou por relatórios online.

Temos aí dois critérios que determinam a “popularidade por distribuição”: o valor de um atributo (status=ABERTO) e a data de criação do objeto. É muito difícil criar um cache que seja inteligente o bastante para manter na memória apenas os registros que interessam – os populares. Uma alternativa é usar referências fracas (com APIs como WeakReference e WeakHashMap). Podemos adicionar objetos ao cache à vontade, pois se faltar memória o coletor de lixo da JVM irá invalidar as referências fracas, apagando os objetos que sejam atingíveis somente por tais referências. Isso não privilegia os objetos populares, mas simplesmente esvazia o cache quando houver pouca memória livre.

Existem algoritmos que promovem referências fracas a referências fortes para os objetos mais acessados (os populares); mas isso começa a complicar as coisas, sem falar que o uso intenso de referências fracas aumenta muito a carga sobre o garbage collector. Por isso, muitas vezes a melhor solução é somente limitar o tamanho da estrutura de dados de cache, e ter uma política de eviction (veja o quadro “Os problemas de caches gigantes”).

Esta versão do cache poderia se beneficiar de mais algumas melhorias. Por exemplo, poderíamos criar uma classe que encapsula a Connection e também fabrica os DAOs. Isso nos permitiria invocar um único método commit(), que faria o commit na conexão e também invocaria os close() sobre todos os DAOs que foram instanciados para esta conexão.

Caches automáticos

Neste ponto, você deve estar se perguntando se a técnica de programação manual de DAOs com caches não é muito trabalhosa. Como se não bastasse programar o BD diretamente com JDBC e SQL, ainda temos que tornar nossos DAOs duas vezes mais complexos com todas essas estruturas de dados e algoritmos de cache – lembrando que caches devem melhorar o desempenho e preservar a consistência, senão, melhor não tê-los! Mas se tivermos que escrever todo esse código adicional para cada uma das dezenas de classes DAO de uma aplicação de médio ou grande porte, parece esforço demais.

A não ser, é claro, que possamos fazer isso de uma só vez, reusando uma implementação genérica em todos os nossos DAOs.

Recapitulando o artigo “A Dinâmica do Java” (Edição 14), uma proxy dinâmica é uma facilidade do J2SE (presente desde a versão 1.3), consistindo na criação automática pela JVM de uma classe que implementa uma lista de interfaces especificada. Proxies dinâmicas são usadas para interceptar invocações a outras classes que implementem a mesma interface; são uma implementação do padrão de projeto Proxy. O artigo da Edição 14 demonstrou as proxies dinâmicas com um exemplo simples de logging, mas podemos ver na Listagem 4 uma aplicação bem mais poderosa dessa facilidade.

A classe CacheHandler implementa os recursos de cache de operações de DAO que vimos anteriormente, mas com uma vantagem: funciona para qualquer DAO. Para isso ser possível só precisamos criar uma interface para cada DAO e instanciar os DAOs através de métodos de fábrica (o que é uma boa opção de design, de qualquer maneira). Uma vez tendo esta arquitetura, os métodos de fábrica podem gerar DAOs que são encapsulados por uma proxy dinâmica, com um CacheHandler associado.

Qualquer invocação ao DAO será interceptada por CacheHandler.invoke(). Antes de delegar a invocação ao método de destino no DAO, o handler pode executar qualquer código. No caso, temos código de cache. Observe que este código é mais complexo que as versões das Listagens 3 e 4, por ser mais generalizado. Suportamos métodos com um número arbitrário de argumentos e utilizamos todos estes argumentos como chaves de cache. Assim, podemos ter consultas com vários critérios de filtro, por exemplo Empresa.find(String localidade, boolean ativa). Uma invocação como find("Curitiba", true) irá consultar e atualizar uma posição de cache única. O objeto Method também é utilizado como chave, havendo um HashMap separado para cada método, de forma que não haverá conflitos se um mesmo DAO possuir diversos métodos com listas de parâmetros iguais.

Observe que a Listagem 4 não implementa um cache de duas camadas, nem outros requisitos de uma versão de produção, como a geração de estatísticas (ex.: taxa de acerto do cache), logs, ou suporte específico a métodos que não retornam nenhum valor e não precisam de cache (como métodos de update do DAO). Estas melhorias seriam desejáveis numa versão de produção deste código.

Mesmo nesse ponto, você pode estar se perguntando se tal investimento em arquitetura vale a pena. Não estaríamos gradualmente implementando nosso próprio “JDO de garagem”? Isso é verdade em parte – estamos usando algumas técnicas comuns em ferramentas O/R. Por outro lado, continuamos trabalhando no nível de banco de dados relacional e não necessariamente no nível de objetos. Uma das grandes deficiências das ferramentas O/R é que suas otimizações mais avançadas beneficiam exclusivamente os códigos mais puros, que só acessam dados do BD através dos objetos Java que os mapeiam. Por exemplo, o cache de memória é organizado em termos de POJOs. Já o nosso CacheHandler não tem nenhum preconceito que favoreça esses objetos comuns. Ele irá funcionar da mesma maneira para uma consulta não-OO, como mostrado na Listagem 5.

Nessa listagem temos uma consulta que não envolve POJOs em nenhum momento – nem nos parâmetros nem nos dados retornados. Isso não faz diferença para o CacheHandler, para o qual os tipos dos parâmetros (chaves do cache) e dos retornos (valores do cache) são irrelevantes. Se você fizer duas invocações consecutivas com ano=2004, a primeira fará uma consulta, mas a segunda retornará instantaneamente do cache. Essa facilidade de cache para consultas ad hoc não é comum em ferramentas O/R (logo veremos porque).

No projeto para o qual desenvolvi esta técnica, temos tarefas tipicamente OLAP onde uma tabela gigantesca (a tabela de "fatos”) deve ter todos seus registros cruzados com uma série de tabelas muito menores (as tabelas de “dimensões”). Pense, por exemplo, numa tabela LANCAMENTO_CONTA com milhões de registros, e uma tabela TAXA com apenas 70 registros. Começamos com uma consulta que retorna os lançamentos de um dia, e vamos iterando o ResultSet e processando os lançamentos. Mas sobre cada lançamento, pode incidir uma taxa diferente. O problema é que não podemos simplesmente ler o lançamento e a taxa de uma só vez com um join, pois não há um código de taxa no registro de lançamento: a taxa deve ser escolhida segundo um conjunto de regras de negócio complexas, o que nos obriga fazer isso com código Java após ter lido o lançamento, e depois ler a taxa com uma nova consulta – uma consulta adicional para cada lançamento. Para piorar, haverá milhares de lançamentos utilizando a mesma taxa, o que torna essas consultas extremamente redundantes.

A solução mais popular para esses casos é ler toda a tabela TAXA para um array em memória no início do processamento (ou da aplicação, se esta tabela for imutável). Então, para cada registro da tabela de fatos, executamos nossas regras de negócio e uma vez identificado o tipo de taxa, obtemos seu valor dessa tabela em memória. Isso funciona, mas é trabalhoso. Tem que ser repetido para cada nova situação semelhante, além de deixar o código mais complicado e facilitar uma série de bugs.

Uma solução muito melhor seria utilizar um DAO para ler as taxas, e uma classe como nosso CacheHandler para evitar consultas repetidas para as mesmas taxas. Com essa mudança, no caso real a que me referi, um determinado lote de trabalho que, sem nenhum cache, demorava 20 minutos para processar, passou a demorar 30 segundos, devido à eliminação de milhares de consultas idênticas.

É importante observar que essa técnica de cache de consultas ad hoc deve ser usada com muita discriminação e cuidado. Ao contrário dos DAOs com cache restrito a POJOs, não existe uma maneira simples e reusável de garantir a consistência entre o cache e os dados no BD. Na Listagem 5, se você invocar calculaMediasAnuais(2004), depois fizer updates em alguns registros de VENDAS (com datas de 2004) e depois invocar novamente calculaMediasAnuais(2004), o resultado será o mesmo de antes, sem considerar os dados alterados.

Existem maneiras de resolver esse problema. Poderíamos limitar o cache a consultas sobre dados históricos, que nunca mais terão atualizações, mas essa é uma solução limitada. Outra idéia seria fazer os métodos de update de OrdemCompraDao limparem o cache das consultas ad hoc, forçando uma nova execução após qualquer update. Isso funciona bem se a consulta lê uma única tabela e esta tabela tem updates pouco freqüentes; mas se eles são muito freqüentes, eliminamos totalmente a vantagem de desempenho do cache. E se a consulta realizar joins entre várias tabelas (que é um dos principais motivos para usarmos consultas ad hoc), a solução se torna quase inviável, pois os DAOs de múltiplas classes precisariam sincronizar o cache uns dos outros, o que exigiria listeners para notificação entre DAOs. Você percebe onde isso vai parar: numa complexidade de arrancar os cabelos.

A moral da história é que a programação direta em JDBC/SQL (sem sistemas O/R) é preferencial quando precisamos de um alto grau de controle, ou de usar truques avançados para obter maior desempenho. Também vemos porque ferramentas O/R não suportam cache de consultas arbitrárias (não limitadas a consultas simples que retornam POJOs, como por exemplo extents e relacionamentos) – embora possuam toda a infra-estrutura necessária. É porque isso é arriscado, e os autores destes sistemas não querem ser inundados por reclamações de programadores que não tomaram algum cuidado e tiveram um bug de consistência de cache ou banco de dados. Caches mais poderosos são uma técnica avançada a ser utilizada com bastante disciplina e precaução.

Conclusões

Neste artigo, procuramos dar nova vida à programação JDBC “na unha”, que pode ser bem menos produtiva que o uso de ferramentas O/R – mas que ainda é extremamente necessária nos cenários onde as opções de mais alto nível não se mostram satisfatórias.

Só porque você precisa executar algumas consultas ou atualizações com JDBC puro, não quer dizer que não possa ter um código organizado, reusável e eficiente. O padrão de projeto DAO é a estrutura fundamental para tornar código JDBC mais administrável, mas também nos fornece uma arquitetura que pode ser estendida para atingir os objetivos de reuso e de alto desempenho. Quando todos os métodos que trabalham com JDBC são encapsulados em DAOs, podemos implementar várias melhorias interessantes trabalhando sobre esses DAOs.

Para fazer isso, o recurso de proxies dinâmicas da plataforma Java é extremamente útil, pois permite “injetar” funcionalidades tão avançadas quanto caching sem alterar uma única linha de código dos DAOs. A mesma técnica poderia ser utilizada para outras finalidades. Por exemplo, para tratamento de erros: poderíamos fazer os DAOs lançaram uma SQLException ao invés de tratá-la, e o handler da proxy dinâmica faria este tratamento, por exemplo logando o erro e retornando um valor nulo ou default, ou lançando uma exceção da aplicação.

Não se sinta obsoleto por programar diretamente em JDBC. Use a melhor API ou ferramenta para cada caso. As opções de mais baixo nível são trabalhosas, mas são também mais poderosas, e suas desvantagens podem ser compensadas pelo uso de técnicas apropriadas.

Os problemas de caches gigantes

Toda vez que a JVM precisa liberar algum espaço no heap, o garbage collector precisa percorrer todo o heap, descobrir quais objetos ainda são úteis e quais são “lixo”, apagar estes últimos e reorganizar os que sobraram. O tempo desta operação numa implementação simples de um garbage collector seria proporcional ao número de objetos.

Mas as JVMs modernas utilizam algoritmos de garbage collection (GC) sofisticados que quase sempre evitam coletar o heap inteiro (o chamado “full-GC”), entre outros truques, que reduzem muito as pausas e o tempo de CPU usado para GC (veja o artigo “Garbage Collection” na Edição 5). Mas esses algoritmos não fazem milagres; em especial, não eliminam totalmente as operações full-GC. Os únicos que aparentemente fazem isso, como os coletores incrementais ou concorrentes, na verdade distribuem os custos de full-GC no tempo.

Ou seja, ao invés de travar a aplicação durante três segundos só para fazer a coleta de lixo uma vez por minuto, um coletor incremental ou concorrente irá gastar os mesmos três segundos por minuto, mas sem nenhum travamento aparente; isso porque a coleta é feita em paralelo ou dividida em muitas fatias pequenas. Mas nada disso muda o fato que a JVM estará torrando 5% do seu tempo de CPU com GC! E se o overhead era de 5% para um heap de 1 Gb, poderá ser de 50% para 10 Gb. Na prática o crescimento é menos que linear com o tamanho do heap, de forma que nossa aplicação hipotética poderia ter um custo de GC de apenas 15% com 10 Gb. Mesmo assim, vale a regra (naturalmente) que quanto maior o heap, maior o tempo gasto com GC.

Isso não significa que a plataforma Java não seja capaz de gerenciar heaps gigantescos. Significa apenas que com heaps enormes pode ser preciso abrir mão de parte dos benefícios do garbage collection e fazer algum grau de gerenciamento manual de memória. O uso de DirectBuffers (introduzidos no J2SE 1.4, veja java.nio.ByteBuffer) permite fazer isso. É uma técnica complexa, mas só quem precisa se aborrecer com isso é o fornecedor do sistema de persistência. Todavia, ainda não existe nenhum produto no mercado, de meu conhecimento, que faça o gerenciamento manual; talvez porque todo mundo concorde, por outros motivos, que raramente é uma boa idéia manter a base de dados inteira na memória. E há mais razões:

  • A complexidade adicional pelo uso de DirectBuffers tornaria os caches menos eficientes que implementações convencionais (que “confiam” no GC), para os objetos que não existem em volumes imensos e dispensam esta solução.
  • Mesmo um cache gigante pode não comportar toda a base de dados. Talvez você tenha 10 Gb de cache, mas 200 Gb de dados no BD. A primeira conseqüência é que você continua correndo o risco de mau desempenho para consultas “pesadas” como nosso exemplo de agrupamento de vendas, pois nunca será garantido que todos os dados estarão no cache. A segunda conseqüência é que o cache precisará de uma estratégia de eviction (“desalojamento” ou “expulsão”). Por exemplo, se só cabem 10 milhões de POJOs no cache, quando este número for atingido e mais cinco objetos forem lidos, é preciso remover outros cinco objetos do cache – preferencialmente os cinco que não são usados há mais tempo, segundo o método de acesso LRU (Last Recently Used). As estratégias de eviction têm um custo, que é pequeno, mas proporcional ao tamanho do cache.
  • Talvez a base de dados toda caiba na memória RAM física, mas certamente não caberá nas memórias cache (L1/L2/L3) do processador, cujo tamanho é no máximo de uns poucos megabytes, mesmo em sistemas high-end. Aplicações com heaps gigantescos, nos quais os dados mais utilizados estejam muito misturados com um volume bem maior de dados raramente utilizados (o que é chamado “baixa localidade”), têm uma péssima eficiência de uso das memórias cache da CPU.

Listagem 1. DAO e VO.


import java.util.Date;

import java.sql.*;

 

public class Venda {

  private final long ID;

  private String codItem;

  private int quantidade;

  private Date data;

  public Venda (...) {...}

  public String getCodItem () { return codItem; }

  public void setCodItem (String codItem) { this.codItem = codItem; }

  //... mais getters e setters ...

}

 

public class VendaDao {

  private final Connection conn;

  public VendaDao (Connection conn) { this.conn = conn; }

  public Venda restore (long ID) {

    PreparedStatement stmt = null;

    try {

      stmt = conn.prepareStatement(

        "SELECT * FROM VENDA WHERE id=?");

      stmt.setLong(1, ID);

      ResultSet rs = stmt.executeQuery();

      if (rs.next()) return new Venda(

        rs.getLong("id"), rs.getString("codItem"),

        rs.getInt("quantidade"), rs.getDate("data"));

    }

    catch (SQLException e) {}

    finally {

      if (stmt != null)

        try { stmt.close(); } catch (SQLException e) {...}

    }

    return null; // exceção ou Venda não encontrada

  }

  public void create (Venda oc) {...}

  public void remove (Venda oc) {...}

  public void update (Venda oc) {...}

}

 

// Exemplos de uso 

Connection conn = null; // ... obtém uma conexão

VendaDao dao = new VendaDao(conn);

Venda venda = dao.restore(99);

venda.setQuantidade(venda.getQuantidade() +1);

dao.update(venda);

conn.commit();

 

Listagem 2. DAO com Cache simples.


public class Empresa {

  private String nome, descricao;

  // Omitidos: construtor, getters, setters

}

 

import java.sql.*;

import java.util.*;

 

public class EmpresaDao {

  private final HashMap empresas = new HashMap();

  private final Connection conn;

  public EmpresaDao (Connection conn) { this.conn = conn; }

 

  public synchronized Empresa find (String nome) {

    Empresa emp = (Empresa)empresas.get(nome);

    if (emp != null) return emp;

    emp = ... // omitido: JDBC para SELECT

    empresas.put(nome, emp);

    return emp;

  }

 

  public synchronized void create (Empresa emp) {

    // omitido: JDBC para INSERT

    empresas.put(emp.getNome(), emp);

  }

 

  public synchronized void remove (Empresa emp) {

    // omitido: JDBC para DELETE

    empresas.remove(emp.getNome());

  }

 

  public synchronized void update (Empresa emp) {

    // omitido: JDBC para UPDATE

    empresas.put(emp.getNome(), emp);

  }

}

Listagem 3. DAO com Cache mais sofisticado.


import java.sql.*;

import java.util.*;

 

public class EmpresaDao {

  private final HashMap globalCache = new HashMap();

  private final ThreadLocal tlCache = new ThreadLocal(){

    protected synchronized Object initialValue () {

      return new HashMap();

    }

  };

  private final Connection conn;

 

  public EmpresaDao (Connection conn) {

    tlCache.set(null);

    this.conn = conn;

  }

 

  public Empresa find (String nome) {

    // Tenta inicialmente localizar o objeto no cache local.

    HashMap localCache = (HashMap)tlCache.get();

    Empresa emp = (Empresa)localCache.get(nome);

 

    if (emp != null) return emp;

    // Tenta localizar no cache global.

    synchronized (globalCache) {

      emp = (Empresa)globalCache.get(nome);

    }

    if (emp == null) {

      // Não localizado nem no cache global; lê do BD.

      emp = ... // omitido: JDBC para SELECT

      if (emp != null) synchronized (globalCache) {

        globalCache.put(nome, emp);

      }

    }

    else emp = (Empresa)emp.clone();

    // Adiciona ao cache local, e retorna o resultado.

    if (emp != null) localCache.put(nome, emp);

    return emp;

  }

 

  public void create (Empresa emp) {

    HashMap cache = (HashMap)tlCache.get();

    if (cache.containsKey(emp.getNome()))

      throw new IllegalStateException("Objeto ja criado");

    // omitido: JDBC para INSERT

    cache.put(emp.getNome(), emp);

  }

 

  public void remove (Empresa emp) {

    HashMap cache = (HashMap)tlCache.get();

    if (cache.containsKey(emp.getNome()) &&

      cache.get(emp.getNome()) == null)

        throw new IllegalStateException("Objeto ja removido");

    // omitido: JDBC para DELETE

    ((HashMap)tlCache.get()).put(emp.getNome(), null);

  }

 

  public void update (Empresa emp) {

    HashMap cache = (HashMap)tlCache.get();

    if (cache.get(emp.getNome()) == null)

        throw new IllegalStateException("Objeto nao existe, ou ja removido");

    // omitido: JDBC para UPDATE

  }

 

  public void close (boolean commit) {

    HashMap localCache = (HashMap)tlCache.get();

    if (localCache == null) return; // close() redundante

    tlCache.set(null); // permite ao garbage collector limpar lixo;

                       // limpa o cache para transações futuras;

                       // e evita custo de close()s redundantes

 

    if (commit) synchronized (globalCache) {

      for (Iterator i = localCache.entrySet().iterator(); i.hasNext(); ) {

        Map.Entry entry = (Map.Entry)i.next();

        Object key = entry.getKey();

        Object value = entry.getValue();

 

        if (value == null)

          globalCache.remove(key);

        else

          globalCache.put(key, value);

      }

    }

  }

}

 

// Exemplo de uso:

Connection conn = null;

EmpresaDao dao = null;

try {

  conn = null;//... omitido: obtém conexão

  dao = new EmpresaDao(conn);

  dao.create(new Empresa("JM", "Java Magazine"));

  conn.commit();   // Se o commit funcionar...

  dao.close(true); // ... sincroniza caches

}

finally {

  if (dao != null) dao.close(false); // Garante que o cache seja limpo

  if (conn != null)

  try { conn.close(); } catch (SQLException e) {}

}

Listagem 4. Cache automático para DAOs.


import java.lang.reflect.*;

import java.lang.reflect.Array;

import java.sql.*;

import java.util.*;

import javax.sql.*;

 

// Handler que implementa cache automático para as invocações a uma classe DAO

public final class CacheHandler

  implements InvocationHandler {

  private final Object proxiedObject;

  private final Map caches;

  private static final HashMap noCacheMap = new HashMap();

 

  // Cria um handler de cache para um DAO

  public CacheHandler (Object proxiedObject) {

    this.proxiedObject = proxiedObject;

    caches = Collections.synchronizedMap(new HashMap());

  }

 

  // Determina se o cache é desativado para um método. Esta opção é feita

  // declarando-se a exceção NoCache entre os throws do método.

  private boolean noCache (Method method) {

    Boolean b = (Boolean)noCacheMap.get(method);

    if (b == null) {

      b = Boolean.FALSE;

      Class[] exceptions = method.getExceptionTypes();

      for (int i = 0; i < exceptions.length; ++i)

        if (exceptions[i] == NoCache.class) {

          b = Boolean.TRUE;

          break;

        }

      noCacheMap.put(method, b);

    }

    return b.booleanValue();

  }

 

  // Handler de invocação de métodos do DAO.

  public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {

    // Suporte a métodos que não devem ter cache algum.

    if (noCache(method)) return method.invoke(proxiedObject, args);

    // Suporte a métodos que devem ter cache.

    Object methodKey = makeKey(args);

    Map methodCache = (Map)caches.get(method);

    if (methodCache == null) {

      caches.put(method, methodCache = Collections.synchronizedMap(new HashMap()));

      return invokeAndCache(methodCache, methodKey, method, args);

    }

    return methodCache.containsKey(methodKey) ? methodCache.get(methodKey) : invokeAndCache(

      methodCache, methodKey, method, args);

  }

 

  // Invoca um método e armazena seu resultado no cache.

  private Object invokeAndCache (Map cache, Object key, Method method, Object[] args)

    throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {

    Object ret = method.invoke(proxiedObject, args);

    cache.put(key, ret);

    return ret;

  }

 

  private static Object makeKey (Object[] args) {

    if (args.length == 0) return Boolean.TRUE;

   

    if (!(args[0] instanceof Connection || args[0] instanceof Statement ||

        args[0] instanceof ResultSet || args[0] instanceof DataSource))

      return args.length == 1 && (args[0] == null ||

        !args[0].getClass().isArray()) ? args[0] : new Key(args);

   

    switch (args.length) {

    case 1:

      return Boolean.TRUE;

    case 2:

      if (args[1] == null || !args[1].getClass().isArray())

        return args[1];

      // else fall-through

    default:

      Object[] key = new Object[args.length -1];

      System.arraycopy(args, 1, key, 0, key.length);

      return new Key(key);

    }

  }

 

  // Esta classe implementa a chave do cache. É baseada no array de

  // parâmetros para o método, mas precisamos fazer uma série de tratamentos

  // especiais para poder usar este array como chave de um Map 

  static class Key {

    private final Object[] args;

    Key (Object[] args) {

      this.args = args;

    }

    // Necessário para podermos usar esta classe como chave de Map.

    public boolean equals (Object obj) {

      if (!(obj instanceof Key)) return false;

      Key other = (Key)obj;

      // Duas chaves serão iguais se os seus arrays de parãmetros forem iguais.

      // Não precisamos testar os tamanhos dos dois arrays, pois este equals()

      // só é invocado para chaves do mesmo cache de método, e todas invocações a

      // um determinado método têm o mwsmo mnúmero de parâmetros.

      for (int i = 0; i < args.length; ++i) {

        // Vamos comparar cada parâmetro com o da outra chave.

        Object a = args[i], b = other.args[i];

        if (a == null && b == null) continue;

        if ((a == null) != (b == null)) return false;

        // Obtém o tipo de cada parâmetro.

        Class ca = a.getClass(), cb = b.getClass();

        if (ca.isArray()) {

          if (cb != ca) return false;

          // Se os parâmetos são arrays, precisamos de todo esse código para

          // fazer a comparação de arrays, o que exige uma versão diferente

          // de Arrays.equals() para cada possibilidade de tipo-base do array.

          if (a instanceof Object[])

            { if (!Arrays.equals((Object[])a, (Object[])b)) return false; }

          else if (ca == int[].class)

            { if (!Arrays.equals((int[])a, (int[])b)) return false; }

          else if (ca == byte[].class)

            { if (!Arrays.equals((byte[])a, (byte[])b)) return false; }

          else if (ca == short[].class)

            { if (!Arrays.equals((short[])a, (short[])b)) return false; }

          else if (ca == long[].class)

            { if (!Arrays.equals((long[])a, (long[])b)) return false; }

          else if (ca == char[].class)

            { if (!Arrays.equals((char[])a, (char[])b)) return false; }

          else if (ca == double[].class)

            { if (!Arrays.equals((double[])a, (double[])b)) return false; }

          else if (ca == float[].class)

            { if (!Arrays.equals((float[])a, (float[])b)) return false; }

        }

        else return a.equals(b); // Para não-array, basta um equals().

      }

      return true;

    }

    // Necessário para podermos usar esta classe como chave de Map:

    public int hashCode () {

      int hash = 0;

      for (int i = 0; i < args.length; ++i)

        if (args[i] != null) {

          // Aqui também damos suporte a parâmetros que são arrays.

          if (args[i].getClass().isArray())

            hash ^= Array.getLength(args[i]);

          else

            hash ^= args[i].hashCode();

        }

      return hash;

    }

  }

 

  /* Tag para métodos que não querem cache. basta incluir esta exceção no

   * throws. Isto simula anotações, mas é compatível com qualquer J2SE. */

  public static final class NoCache extends RuntimeException {

    private NoCache () { }

  }

}

 

// Interface do DAO

public interface EmpresaDao {

  Empresa create (String nome) throws SQLException;

}

 

// Fábrica abstrata de DAOs da aplicação

public class FactoryDao {

  public static EmpresaDao createEmpresa () {

    EmpresaDao dao = new EmpresaDaoImpl();

    try {

      return (EmpresaDao)Proxy.newProxyInstance(

          EmpresaDaoImpl.class.getClassLoader(),

          new Class[]{EmpresaDao.class},

          new CacheHandler(dao));

    }

    catch (IllegalArgumentException e) {

      System.err.println("Falha criando proxy! Retornando DAO sem cache.");

      return dao;

    }

  }

}

 

// Implementação do DAO

class EmpresaDaoImpl implements EmpresaDao {

  public Empresa create (String nome) {

    ... // implementação omitida

  }

}

Listagem 5. DAO com consulta ad hoc.


import java.sql.*;

import java.util.HashMap;

import java.util.Map;

 

public class VendaDao {

  public Map calculaMediasAnuais (Connection conn, int ano)

    throws SQLException {

    PreparedStatement stmt = conn.prepareStatement(

      "SELECT codItem, TRUNC(data, 'yyyymmdd')," +

      " AVG(quantidade) FROM VENDAS" +

      " WHERE data >= ? AND data < ?" +

      " GROUP BY codItem, TRUNC(data, 'yyyymmdd')");

    stmt.setDate(1, new Date(1900+ano,0,1));

    stmt.setDate(2, new Date(1901+ano,0,1));

    ResultSet rs = stmt.executeQuery();

    HashMap ret = new HashMap();

    while (rs.next())

      ret.put(rs.getString(1), new Double(rs.getDouble(2)));

    return ret;

  }

}
Glossário
  • Algumas empresas implementaram conceitos de ferramentas cliente/servidor para Java, como componentes “data-aware” (por exemplo, o dbSwing do JBuilder). Mas estas opções, entre outras mais tradicionais (como SQL embutido), acabaram não fazendo muito sucesso na comunidade Java.
  • Esta consulta SQL funciona no Oracle. No MySQL, por exemplo, substitua TRUNC(data, 'yyyymmdd’) por DAYOFYEAR(data).
  • Os SGBDs costumam ter um cache de tabelas, que elimina muito do custo de I/O físico das queries. Mas não elimina os custos também consideráveis de execução de SQL, comunicação entre a aplicação e o SGBD, e conversão entre POJOs e dados brutos de resultsets e statements.
  • No J2SE 5.0, isso ficaria melhor com tipos genéricos: HashMap .

Saiu na DevMedia!

  • React com Redux:
    O Redux atende as necessidades de pelo menos um cenário comum em aplicações cliente, facilitando a comunicação entre componentes sem acoplá-los. Sua importância é tanta atualmente que muitos programadores têm aconselhado seu uso independente do tamanho da aplicação, embora ele facilite o seu crescimento.
  • Autenticação em Aplicações Web:
    Tornar algumas páginas acessíveis apenas a um grupo de usuários autenticados é uma tarefa trivial em aplicações web. Existem diferentes frameworks para isso, mas a maioria deles cobre desde o cadastro até as credenciais, passando pela autenticação e controle de acesso.

Saiba mais sobre Java ;)

  • Cursos de Java:
    Torne-se um programador Java completo. Aqui você encontra cursos sobre as mais ferramentas e frameworks do universo Java. Aprenda a desenvolver sites e web services com JSF, Jersey, Hibernate e mais.
  • Programador Java:
    Aprender Java não é uma tarefa simples, mas seguindo a ordem proposta nesse Guia, você evitará muitas confusões e perdas de tempo no seu aprendizado. Vem aprender java de verdade, vem!
  • Revista Java Magazine Edição 25
    Confira nesta edição de Java Magazine dados turbinados, aumente a performance de acesso a dados em suas aplicações usando JDBC avançado, DAOs otimizados e caching de dados. Veja também o novo XP explicado, saiba o que está mudando no extreme programming em uma resenha detalhada.