Canguru - Persistência de Objetos em Java

Equipe:
Felipe Gustavo de Almeida
Fernanda Simões de Almeida
Maíra de Assis Ramos

Orientador: Prof. Francisco Reverbel (http://www.ime.usp.br/~reverbel/)

Índice

Apresentação do problema

Uma das principais dificuldades encontradas pelos desenvolvedores de aplicações orientadas a objeto é a persistência dos dados. Em muitos casos opta-se por utilizar bancos relacionais e, com isso, surge o problema de mapear os objetos para o banco.

As soluções geralmente adotadas envolvem descrever uma relação entre as propriedades do objeto e os campos de uma ou mais tabelas no banco relacional. Essa relação pode ser descrita através de queries SQL, em que a aplicação é responsável por persistir e recuperar os objetos direto no banco, ou através de ferramentas mais sofisticadas como o Prevayler, Hibernate, ou mesmo Enterprise Java Beans.

Quando o desenvolvedor opta por fazer a persistência "manualmente" usando queries, perde-se muito tempo desenvolvendo código para recuperar e salvar os dados em banco e a aplicação fica muito vulnerável a pequenos erros de programação típicos de trabalhos repetitivos e copy-and-paste.

A maioria das ferramentas existente para resolver esse problema requer que o usuário descreva o mapeamento OO-Relacional, por exemplo em XML, implemente interfaces e obedeça a uma série de restrições. Para projetos grandes, algumas delas são muito boas porém, para aplicações menores acabam introduzindo muita complexidade.

Objetivos

O objetivo desse projeto é construir um arcabouço de simples utilização que facilite o trabalho do desenvolvedor, tornando o processo de persistir dados o mais transparente possível e sem onerar excessivamente os recursos da máquina.

O público alvo do Canguru é o desenvolvedor de aplicações que não necessita ou não queira toda a complexidade de um sistema como, por exemplo, o Hibernate.

Para tal, o Canguru deve apresentar as seguintes características:

Solução

O Canguru é um arcabouço em Java para persistência e recuperação de dados, que se baseia em uma forma transparente de fazer a conversão entre Objetos de aplicações Java e registros salvos no banco de dados, oferecendo uma interface simples derivada de java.util.Set com métodos adicionais para salvar, recuperar e fazer busca por objetos.

Utilizando o Canguru

Para utilizar o Canguru, o desenvolvedor tem apenas dois contatos com o banco de dados: a criação da base (somente da base) e do arquivo de configuração do Canguru como abaixo.

database.prp:


  #Configurações para conexão com o banco de dados
  driver=org.postgresql.Driver
  url=jdbc:postgresql://127.0.0.1:5432/databasename
  user=usuario
  password=senha
      

Nesse arquivo são informados os dados para a conexão com o banco de dados:

Com a conexão para o banco configurada, a aplicação cliente deve instanciar um objeto da classe Canguru, informando qual o tipo (classe ou interface) dos objetos que serão armazenados. Pode-se utilizar diversos cangurus para armazenar objetos de tipos diferentes. A partir disso, as funcionalidades do arcabouço são acessadas através de três interfaces (fig. 1) disponibilizadas na classe Canguru:

Dessa forma, a aplicação consegue inserir elementos em uma Collection (Canguru), solicitar que sejam salvos em banco quando necessário e recuperá-los total ou parcialmente, através de filtros.

imagem1.png
Figura 1

Para detalhar a utilização do Canguru, vamos usar o exemplo abaixo:

CanguruExample.java:

  1: import java.sql.SQLException;
  2: import java.util.Iterator;
  3: import java.util.Set;
  4: import canguru.Canguru;
  5: import canguru.MixurucaEnhanced;
  6: import canguru.descriptor.exception.AttributeDefinitionNotFoundException;
  7: import canguru.descriptor.exception.InvalidAttributeException;
  8: import canguru.descriptor.exception.InvalidNameException;
  9: import canguru.exception.BeanManipulationException;
 10: import canguru.exception.CanguruInitializationException;
 11: import canguru.exception.FilterException;

 13: /**
 14:  * Pequeno exemplo de uso do Canguru.
 15:  */
 16: public class CanguruExample {

 18:     CanguruExample() {
 19:     }

 21:     public void run() {

 23:         try {
 24:             // inicializa o canguru, os parâmetros são:
 25:             // um String que servirá como identificador para essa coleção
 26:             // um Class  que indica qual a classe dos objetos que o canguru deverá aceitar 
 27:             Canguru canguru = new Canguru("exemplo", MixurucaEnhanced.class);

 29:             // cria alguns objetos para serem adicionados (da mesma classe que foi passada 
 30:             // para o construtor do Canguru)
 31:             MixurucaEnhanced mixuruca1;
 32:             MixurucaEnhanced mixuruca2;
 33:             MixurucaEnhanced mixuruca3;

 35:             //atribui valores a algumas das propriedades dos objetos criados
 36:             mixuruca1 = new MixurucaEnhanced();
 37:             mixuruca1.setStringTest("teste");

 39:             mixuruca2 = new MixurucaEnhanced();
 40:             mixuruca2.setStringTest("teste");
 41:             mixuruca2.setAInteger(new Integer(42));

 43:             mixuruca3 = new MixurucaEnhanced();
 44:             mixuruca3.setAInteger(new Integer(42));

 46:             // adiciona os objetos ao Canguru
 47:             canguru.add(mixuruca1);
 48:             canguru.add(mixuruca2);
 49:             canguru.add(mixuruca3);
 50:             canguru.add(new MixurucaEnhanced());

 52:             try {
 53:                 //salva os dados
 54:                 canguru.save();
 55:             }
 56:             catch (ClassNotFoundException e1) {
 57:                 e1.printStackTrace();
 58:             }
 59:             catch (SQLException e1) {
 60:                 e1.printStackTrace();
 61:             }
 62:             catch (java.io.IOException e1) {
 63:                 e1.printStackTrace();
 64:             }
 65:             catch (canguru.exception.IOException e1) {
 66:                 e1.printStackTrace();
 67:             }
 68:         }
 69:         catch (CanguruInitializationException e) {
 70:             //a inicialização do canguru falhou
 71:             e.printStackTrace();
 72:         }

 74:         //recuperando os dados
 75:         try {
 76:             //inicializa um novo canguru utilizando o mesmo string como identificador e o mesmo class,
 77:             // assim poderemos recuperar os dados salvos pela outra instância do Canguru.
 78:             Canguru canguru = new Canguru("exemplo", MixurucaEnhanced.class);

 80:             try {
 81:                 //recupera todos os objetos objetos salvos em banco
 82:                 canguru.restore();

 84:                 //percorre os elementos
 85:                 Iterator it = canguru.iterator();
 86:                 while (it.hasNext()) {
 87:                     System.out.println(it.next());
 88:                 }

 90:                 try {
 91:                     // busca elementos especificos
 92:                     canguru.addFilter("stringTest", "teste");
 93:                     Set filtered = canguru.getFilteredSubset();
 94:                     //nesse ponto o filtered possui referencia para os dois objetos que possuem 
 95:                     //a propriedade stringTest == teste

 97:                     //restringindo mais a busca
 98:                     canguru.addFilter("AInteger", new Integer(42));
 99:                     filtered = canguru.getFilteredSubset();
100:                     //agora o filtered soh possui referencia para um objeto que possui stringTest == test 
101:                     //e AInteger == 42

103:                     //limpa os filtros
104:                     canguru.resetFilter();
105:                     //adiciona outro filtro
106:                     canguru.addFilter("AInteger", new Integer(42));
107:                     filtered = canguru.getFilteredSubset();
108:                     //agora o filtered possui referencia para dois objetos com AInteger == 42

110:                 }
111:                 catch (AttributeDefinitionNotFoundException e2) {
112:                     e2.printStackTrace();
113:                 }
114:                 catch (InvalidNameException e2) {
115:                     e2.printStackTrace();
116:                 }
117:                 catch (InvalidAttributeException e2) {
118:                     e2.printStackTrace();
119:                 }
120:                 catch (FilterException e2) {
121:                     e2.printStackTrace();
122:                 }

124:             }
125:             catch (java.io.IOException e1) {
126:                 e1.printStackTrace();
127:             }
128:             catch (ClassNotFoundException e1) {
129:                 e1.printStackTrace();
130:             }
131:             catch (SQLException e1) {
132:                 e1.printStackTrace();
133:             }
134:             catch (BeanManipulationException e1) {
135:                 e1.printStackTrace();
136:             }
137:         }
138:         catch (CanguruInitializationException e) {
139:             e.printStackTrace();
140:         }
141:     }

143:     public static void main(String[] args) {
144:         new CanguruExample().run();
145:     }
146: }

Na linha 27 inicializamos uma instância do Canguru passando como parâmetros um string exemplo e uma classe MixurucaEnhanced.class. O string tem a função de identificador para a coleção, a classe indica qual o tipo dos objetos que essa instância deverá aceitar. Cada instância pode trabalhar apenas com um tipo de objeto.

Entre as linhas 29 e 44 criamos algumas instâncias de objetos da classe MixurucaEnhanced e atribuímos valores a algumas de suas propriedades. A seguir adicionamos esses objetos ao Canguru (linha 47).

Para salvar todos os objetos em banco usamos canguru.save() na linha 54 e ao executar esse método o Canguru irá criar automaticamente as tabelas necessárias.

A próxima etapa é recuperar os dados, para fazê-lo vamos utilizar uma nova instância do Canguru, que deve ser inicializada (linha 78) com os mesmos parâmetros da instância que usamos para salvar. Repare que não estamos usando a instância anterior apenas para ilustrar. É perfeitamente possível carregar os dados do banco na mesma instância que foi usada para salvá-los.

Após inicializado o Canguru, para recuperar os dados, basta chamar o método canguru.restore() (linha 82), para percorrer os objetos recuperados usamos os métodos disponíveis em java.util.Set, conforme ilustrado na linha 85. Adicionalmente dispomos de um sistema de filtros que permite buscar objetos pelos valores de suas propriedades (linhas 92 a 107).

Arquitetura

O Arcabouço Canguru é dividido em três módulos (fig. 2):

imagem2.png
Figura 2

Para obter informações detalhadas sobre esse módulo, consulte a API do arcabouço Canguru.

Canguru

O Canguru possui duas classes mais importantes: Canguru e CanguruSaver, a primeira é a interface que o desenvolvedor usa, a segunda é a classe que trata os objetos para salvá-los e recuperá-los.

Quando o método save() é chamado o Canguru passa a Collection com os objetos para o CanguruSaver, que por sua vez os percorre como um grafo, utilizando o java.beans.Intrsopector para fazer uma espécie de numeração topológica, atribuindo ids e detectando ciclos.

Para atribuir ids utilizamos o PMap que é um Map java que usa "==" no lugar de .equals() para fazer as comparações. Algum tempo depois de termos feito essa implementação encontramos o artigo Long-Term Persistence for JavaBeans de MILNE, Philip e WALRATH, Kathy que sugere uma implementação semelhante.

Uma vez percorridos e devidamente identificados (por ids) os objetos são analisados e os valores e ids de suas propriedades são salvos dentro de um Descriptor juntamente com sua forma serializada.

Para recuperar os objetos o CanguruSaver os deserializa do banco, guardando cada uma de suas propriedades e o próprio objeto em um PMap (na realidade é usado um ObjectTable, que facilita o uso do PMap), assim o Canguru pode identificar propriedades que referenciem uma mesma instância, e "corrigir os ponteiro".

Os filtros são pares (atributo, valor) enviados para o Descriptor que os usa como critério de seleção na busca no banco.

É importante ressaltar que o tratamento de referências só é feito no objeto que é passado diretamente como parâmetro para o método add(Object o) do Canguru, isso irá causar problemas ao salvar grafos mais complexos de objetos.

Descriptor

O Módulo Descriptor é responsável por receber as solicitações de persistência, recuperação e busca do Canguru e repassá-las para o Proxy Database de forma adequada. A classe Descriptor serve como fachada para todo o pacote e é a única classe que pode ser acessada pelos outros módulos.

Para o entendimento desse módulo são necessárias três definições:

imagem3.png
Figura 3

Quando um Canguru é criado, ele instancía o descriptor que utilizará, e o configura para realizar suas operações de persistência, recuperação e busca. São definidos:

A partir do momento em que o Descriptor está configurado, são oferecidos três conjuntos de operações ao Canguru. O Canguru pode adicionar os elementos a serem persistidos e solicitar que sejam salvos no banco de dados. Pode solicitar que o Descriptor carregue os elementos a partir do banco do dados para depois carregá-los para a aplicação. Ou ainda, pode inserir filtros de busca, fazer a pesquisa e recuperar os elementos retornados. Todas essas operações são, por sua vez, delegadas ao Proxy Database associado ao Descriptor.

Proxy Database

Na camada de proxy para o banco de dados são realizadas todas as operações de banco delegadas pelo descriptor. Para realizar essas operações, o proxy utiliza as configurações definidas no descriptor associado. Além da conexão com o banco de dados, as seguintes operações são realizadas:

Ferramentas, técnicas e padrões utilizados

Como decidimos trabalhar separadamente, cada um em sua casa, precisamos de ferramentas tanto para integração do código em si, como para integração da equipe, permitindo discussões sobre as soluções encontradas, definição de próximos passos e divisão de tarefas. Para o controle de versões de código utilizamos o CVS, que permite manter o código sincronizando e íntegro, mesmo com alterações concorrentes de vários programadores. Para solucionar os problemas de comunicação, criamos uma lista de discussão, um WiKi e utilizamos massivamente o telefone.

Para programar, utilizamos o Eclipse, que oferece uma série de recursos interessantes, entre eles padronização de código, geração automática do esqueleto para Javadoc e os métodos mais comuns de refatoração de código. Utilizamos também o JUnit integrado ao Eclipse para realizar os testes de Unidade da aplicação.

O arcabouço Canguru foi desenvolvido utilizando o J2SE 1.4.2 e foi testado para o SGBD Postgres 7.3

No desenvolvimento utilizamos, ainda, alguns padrões de projeto. Para instanciar a subclasse adequada do Tipo de Atributo utilizamos o padrão Factory e procuramos utilizar Façades entre os subsistemas da aplicação.

Restrições

Organização da equipe e metodologia de desenvolvimento

A equipe é composta por Felipe Gustavo de Almeida, Fernanda Simões de Almeida e Maíra de Assis Ramos.

Os estudos iniciais sobre o projeto, como introspecção, ferramentas relacionadas ao assunto, ferramentas que poderiam auxiliar o projeto, foram feitos individualmente e, posteriormente, discutidos com toda a equipe.

Já toda a fase de modelagem foi marcada por reuniões da equipe toda para discutir e definir as diretrizes a serem tomadas. Assim que conseguimos uma modelagem mais precisa do projeto, pudemos fazer uma divisão inicial das tarefas entre os componentes do grupo, sendo cada integrante responsável por determinadas atividades que poderiam ser realizadas separadamente. Entretanto, toda solução encontrada foi discutida na busca de aprimoramentos ou novas idéias, assim como a própria divisão de tarefas e objetivos foram rediscutidos e adaptados ao logo do desenvolvimento.

Para que a equipe pudesse trabalhar separadamente, foi essencial o uso de um método de controle de versões (CVS), assim como a criação de uma lista de discussão, que serviu como canal de comunicação entre os membros da equipe para avisar como estava o andamento do projeto e discutir sobre dúvidas que surgiram durante a implementação. Outro elemento de grande importância na nossa comunicação foi o telefone, permitindo que grande parte das decisões fosse feita "on-line", visualizando o código e discutindo.

A divisão inicial do trabalho previa o Gustavo trabalhando no módulo Canguru, a Maíra desenvolvendo a camada de persitência no banco de dados, que incluía tanto o Descriptor, quanto a camada de Proxy para o banco de dados e a Fernanda desenvolvendo a aplicação exemplo para o Canguru.

No andamento do projeto, encontramos dificuldades em desenvolver a aplicação cliente em paralelo com o desenvolvimento do Canguru em decorrência de alterações na interface do Canguru e do volume de desenvolvimento em outros módulos. Decidimos priorizar o desenvolvimento do arcabouço com testes automatizados utilizando o JUnit. A estrutura inicial para os testes, bem como toda a estrutura de comunicação (Wiki + Lista) e controle de fontes (CVS) foi desenvolvida pelo Gustavo.

Com a arquitetura mais madura, surgiu a definição dos três módulos existentes. O módulo Canguru, que inclui implementação da interface Set, introspecção, serialização e recuperação de referências, ficou a cargo do Gustavo. Já o módulo Proxy Database, que inclui a parte de comunicação com o banco de dados, a criação de tabelas e geração de comandos SQL para persistir, recuperar e buscar os objetos ficaram por conta da Maíra, com grande participação do Gustavo. Por sua vez, o módulo Descriptor, responsável pela conversão Banco-OO e pela definição das buscas, foi desenvolvido pela Maíra e pela Fernanda.

Na fase de pré-entrega do projeto, todos participaram executando e melhorando os testes de forma que todo o arcabouço fosse examinado. Assim que alguma falha era percebida, ela era corrigida independentemente que quem havia desenvolvido o código primeiramente. Aproveitamos para utilizar algumas técnicas de refatoração oferecidas pelo Eclipse para prover melhorias no código. O CVS e, principalmente, a comunicação por e-mails e telefone foram cruciais para a sincronização das alterações.

Prazos e andamento

O cronograma original de desenvolvimento era:
Julho: Estudo mais detalhado e modelagem do sistema.
Agosto: Introspecção, grafo de objetos, geração do banco, gravação de objetos.
Setembro: Busca, recuperação, proxy/memento e API.
Outubro: Ajustes, testes e aplicação exemplo.
Novembro: Apresentação e monografia.

Apesar dos esforços esse planejamento não foi respeitado. No início tínhamos feito uma modelagem vaga demais, sobravam muitas decisões para a etapa de implementação. Também gastamos muito tempo com protótipos do sistema de instrospecção.

Em setembro, utilizando o conhecimento que havíamos adquirido com a fase inicial do desenvolvimento, remodelamos o sistema de uma forma mais precisa e modularizada. Os benefícios foram imediatos: a divisão de tarefa, que antes era algo difícil, se tornou um processo natural, e as dúvidas, antes muito comuns durante o desenvolvimento, passaram a ser mais raras de de fácil resolução.

O desenvolvimento passou a ter um ritmo muito bom, porém já não tínhamos muito tempo e itens como a utilização de proxies e a aplicação exemplo, que faziam parte da proposta inicial, tiveram que ser deixados de lado.

Considerações finais

O resultado do projeto, o Canguru, agradou a toda a equipe, pois é uma ferramenta útil e de simples uso, que não exige configurações complicadas ou implementação de interfaces por parte do usuário, como era nosso objetivo inicial.

A intenção do grupo é continuar o seu desenvolvimento, aprimorando aquilo que já possuímos e incorporar novos recursos ao Canguru, tendo sempre em mente sua principal característica, que é a simplicidade. Alguns pontos que seriam interessantes no Canguru e que merecem ser estudados são:

Desafios e frustrações encontrados

Muito embora durante o curso eu tenha feito um estágio reconhecido pelo IME, eu optei por não escrever uma monografia baseada neste estágio e sim fazer um projeto para que eu pudesse ter a oportunidade de enfrentar novos desafios e concluir o curso com uma bagagem de conhecimento maior.

De fato, no projeto, tive a oportunidade de trabalhar com instropecção dos objetos, que era algo novo para mim, com a ferramenta JUnit para a geração dos testes, que não era utilizada na empresa na qual havia trabalhado, por exemplo.

No entanto, a proposta do projeto, de fazer um arcabouço voltado para desenvolvedores de modo a facilitar a tarefa de persistir os dados, foi o que me atraiu a fazer este projeto, até porque eu mesma já tinha vivenciado essa dificuldade desenvolvendo aplicativos orientados a objeto e esse foi o maior desafio do projeto: fazer algo simples, de aplicação prática e que tornasse a persistência de objetos o mais transparente possível ao usuário.

Disciplinas do BCC e o projeto

No desenvolvimento deste projeto, foram importantes as disciplinas:

O trabalho em equipe

O fato de alguns dos membros da equipe já terem trabalhado juntos fez com que o entrosamento entre os mesmos fosse facilitado, proporcionando uma maior agilidade no desenvolvimento do projeto.

Por outro lado, tivemos de enfrentar o problema de conseguir conciliar os horários entre os membros da equipe, uma vez que todos estavam trabalhando e cada qual em um horário diferente. Neste ponto, o uso de comunicação eletrônica (primeiro e-mails individuais, depois uma lista de discussão da equipe, e icq), assim como telefonemas e controle de versão (CVS) foram imprescindíveis para a organização e divisão das tarefas entre os membros da equipe.

Conclusão

Fazer este projeto foi válido uma vez que ele proporcionou que eu tivesse contato com novos conceitos e pudesse aumentar os conhecimentos que adquiri na faculdade e nas empresas nas quais trabalhei.

Mesmo não tendo feito o trabalho de formatura sobre o estágio que realizei, gostaria de ressaltar neste momento que considero importante que os alunos entrem no mercado de trabalho enquanto ainda fazem o curso. Afinal, o trabalho permite que se adquira agilidade, responsabilidade, aprendendo a lidar com prazos, além de, ao colocar o aluno em contato com pessoas com diferentes formações e graus de experiência, proporciona uma troca de conhecimentos extremamente interessante.

Referências

[1] API Canguru (javadoc), api/index.html
[2] API Java 1.4.2 (javadoc), http://java.sun.com/j2se/1.4.2/docs/api/
[3] Documentação do PostgreSQL 7.3, http://www.postgres.org/docs/7.3/static/index.html
[4] Java Beans, http://java.sun.com/products/javabeans/
[5] Especificação do Java Beans, http://java.sun.com/products/javabeans/docs/spec.html
[6] MILNE, Philip e WALRATH, Kathy. Long-Term Persistence for JavaBeans
[7] JOHNSON, Mark. Make JavaBeans mobile and interoperable with XML
[8] Enterprise Java Beans, http://java.sun.com/products/ejb/
[9] Prevayler, http://www.prevayler.org/
[10] Hibernate, http://hibernate.sourceforge.net/
[11] JBoss, http://www.jboss.org/
[12] JMangler, http://javalab.cs.uni-bonn.de/research/jmangler/
[13] Javassist, http://www.csg.is.titech.ac.jp/~chiba/javassist/
[14] OGNL, http://ognl.org/
[15] JXPath, http://jakarta.apache.org/commons/jxpath/index.html
[16] JUnit, http://www.junit.org/
[17] CVS, http://www.cvshome.org/


Dezembro de 2003