Artigo Java Magazine 25 - Persistência Turbinada

Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Para efetuar o download você precisa estar logado. Clique aqui para efetuar o login
Confirmar voto
0
 (1)  (0)

Artigo publicado pela Java Magazine.

Esse artigo faz parte da revista Java Magazine edição 25. Clique aqui para ler todos os artigos desta edição

 

 

Atenção: por essa edição ser muito antiga não há arquivo PDF para download.Os artigos dessa edição estão disponíveis somente através do formato HTML.

 

Byte Code

Persistência Turbinada

DAOs otimizados, Caching e JDBC Avançado

A programação de mais baixo nível com JDBC pode ser a melhor solução para o acesso e atualização de dados e os conhecimentos valem para soluções O/R

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.

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.

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[2]:

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.

"

A exibição deste artigo foi interrompida :(
Este post está disponível para assinantes MVP

 
Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Receba nossas novidades
Ficou com alguma dúvida?