sexta-feira, 23 de agosto de 2013

Como criar uma cópia de um objeto persistente de forma automática (JPA, HIBERNATE, ETC...)

Abaixo está o link para baixar o componente 


Download:  LINK

 Há um tempo atrás, em 2011, precisei implementar em um projeto o conceito de versionamento de registros no banco. A medida que o usuário alterava algumas entidades, o sistema deveria na verdade gerar uma copia do objeto original contendo as alterações realizadas, incrementando o número de versão do registro e efetuar um novo insert no banco ao invés de um update. Isso manteria as versões anteriores a alteração realizada. Ontem, um amigo aqui do trabalho me solicitou ajuda com o mesmo problema. E eu vi que mais pessoas poderiam estar precisando disso. Então resolvi postar aqui para ajudar a quem necessite.

O problema 


 Inicialmente pensasse logo que é algo simples de se fazer. Algo como 'apenas seto o id para null e mando fazer um novo insert'. Mas não é bem assim. Pensar dessa forma é dar um tiro no pé. Isso vai gerar inconsistência na base e problemas no sistema. Porque? O problema é que ao fazermos uma copia fiel do objeto persistente nós queremos todo o contexto persistente do mesmo. Isso quer dizer que precisamos de todos os seus relacionamentos replicados também. E todos os relacionamentos dos relacioanamentos, dos relacionamento, dos relacionamentos, enfim.... precisamos replicar toda a árvore de relacionamentos do objeto original. E quando falo de relacionamentos são de todos os tipos, 1 x N, N x N, 1 x 1, enfim... Exemplo do problema. Para exemplificar, vou mostrar um exemplo com 1 x N, mas que segue a mesma necessidade, problema e solução para os outros tipos de relacionamento. Imagine que temos 3 tabelas e suas respectivas entidades. Pessoa, Carro e Multas. Onde os relacionamentos são como abaixo: Pessoa 1 x N Carro Carro 1 x N Multa O problema é que o lado N sempre tem uma referência ao lado 1, pois esse lado 1 é a chave estrangeira ao qual o relacionamento pertence. Então Carro tem uma propriedade Pessoa, que é o dono do carro. E Multa tem uma propriedade Carro, que é o carro que tem a multa. Então imagine as seguintes instancias.


Observe que a pessoa é proprietaria de dois carros. Cada carro tem uma referência para a pessoa de id=1. Cada carro tem sua lista de multas. Onde cada multa tem uma referência que para o carro ao qual a multa pertence. Dito isto, o problema em simplesmente fazer uma copia da pessoa copiando as propriedades do original para o novo é que os carros contém a referência a pessoa original, bem como suas multas contem uma referencia a seu respectivo carro original. Desta forma, apenas fazer a copia e setar null no ID para que o Hibernate ou JPA faça um novo insert, vai causa inconsistência no banco, pois o relacionamento entre a pessoa e os carros, bem como carros e as multas, continua apontando para suas respetivas referências originais, e não para a copia. Vou dar um exemplo de dois casos que podem acontecer.

  - Caso 1

 Se nós fizermos uma cópia da pessoa original, copiarmos as propriedades do original para a copia e setarmos null no id da copia de modo a forçar o hibernate ou jpa a fazer um novo insert, vamos ver como a figura ficará. Abaixo, coloquei a mesma figura anterior só que marquei a instancia original de pessoa com a letra X, para que diferenciemos a original da copia, quando a fizermos. Então, observe que a pessoa abaixo, é a original e é a instancia X.


Observe que os carros apontam para a pessoa (proprietário) correta e que ao qual realmente pertencem. Instancia X. Agora se fizermos uma copia e criarmos outra instancia de Pessoa, teremos a instância Y, essa instância é a copia e nós trasnferiremos tudo da original para ela. Setando o id como null, queremos forçar ao mecanismo de persistência adotado, que se faça um novo insert. Então passando as propriedades da original para a nova instancia, teremos a instancia Y de pessoa como abaixo:


Observe que agora temos a instância Y e pegamos todas as propriedades da instancia X e passamos para a Y. Observe que os carros são os mesmos da instância X, então eles contém como referência de proprietário a pessoa original (instância X) e suas multas são as mesmas do original, e apontam para seus respectivos carros da pessoa X, e não da Y. Na verdade o correto seria termos novas instâncias dos carros, apontando para a copia da pessoa, a instancia Y, e novas instâncias de multas apontando para estes novos carros e por ai sucessivamente. Persistir a pessoa da forma como está na imagem, se não ocasionar em nenhum erro na validação dos dados por parte do framework de persistencia adotado, ocasionaria no insert apenas de uma nova pessoa, só que essa nova pessoa não teria nenhum carro, pois não existe relacionamento com nenhum carro. Pois os carros apontam para a instancia X e já foram persistidos. O mesmo aconteceria com as multas, que já foram persistidas e apontam para os carros da pessoa original X.

  - Caso 2

 A outra forma que você poderia pensar em fazer, seria ao invés de criar uma nova instância da Pessao X, usar a mesma instância e apenas setar null no ID. Como os outros objetos carro se referem a pessoa por referência, então mudar o id da instancia X, refletiria na mudança em todos os proprietarios dos carros. Então no momento do insert, eles apontariam para o carro correto.

 Ai é que você se engana. Você se esquece que os carros e as multas possuem seus próprios id´s já setados,como mostrado na imagem, pois já foram persistidos e foram recuperados junto com a pessoa para que fosse feito o versionamento. Fazer um update em pessoa dessa forma resultaria em um update, pois eles ja possuem id e ja existem na base, nos objetos Carro alterando a FK (referencia) do proprietario. Ou seja, os carros deixariam de ser do proprietario X para serem da nova pessoa inserida no banco. Então o relacionamento da pessoa original seria perdido.

 Outro problema seria que um objeto recuperado do banco com hibernate ou jpa, eles vem com um PROXY, que gerenciam todo o acesso a classe real. São estes proxies que fazer e tornam possível por exemplo, o mecanimso de LAZY, que é a consulta ao banco do objetos que compoem a entidade, apenas quando necessário, quando chamado o método get. Isso é o proxy que faz pois ele intercepta a chamada ao metodo. Isso é transparente para você. Este mesmo proxy garante que a primary key (ID) não seja alterado. Então uma tentativa de setar null em um objeto recuperado da base resultará numa exceção. Remover o proxy é possível mas isso não resolverá o problema.

 Para este pequeno exemplo, o correto algoritmo seria o algoritmo abaixo:

1 - Criar uma nova instancia de pessoa
2 - Copiar todas as propriedades menos o ID a lista de carros
3 - Criar uma nova instancia de cada carro existente na lista original de carros da pessoa original
4 - Copiar cada propriedade de um carro para o outro, menos o ID e a lista de multas
5 - Criar uma nova instancia de cada multa existente em cada carro na lista original de multas do carro original
6 - Setar a referência ao carro ao qual a multa pertence como a nova copia do carro original
7 - Adicionar as copias das multas do carro original a cada copia de cada carro criado
8 - Adicionar as copias dos carros criados a nova pessoa criada
9 - Fazer o insert

 Isso resultará em:
- 1 insert de uma nova pessoa
- 2 inserts de novos carros apontando para esta nova pessoa
- 4 inserts de novas multas. Sendo 2 multas apontando para o primeiro carro e 2 multas para o segundo 

Observe quanta coisa você teria que fazer na mão e só para replicar o contexto persistente dessa simples classe pessoa. E mesmo essa simples classe, qualquer alteração nos relacionamentos, a adição de mais relacionamentos 1 x N ou N x N ou 1 x 1, a remoção de outros, etc.. resultaria na mudança e na revisão de todo este algoritmo.

 Leve em consideração também que o algoritmo muda de acordo com o tipo de relacionamento. De acordo com o mapeamento realizado na classe. Por exemplo, em um relacionamento 1 x 1, a FK pode estar mapeada na própria classe ou no outro lado do relacionamento. Se for do outro lado, o algoritmo tem que mudar a referencia da instancia do outro lado do relacionamento, para a nova instancia (copia) criada. Se for N x N, o algoritmo é outro, e por ai vai.

 Agora imagine ter que manter isso. Agora imagine que você precise disso em várias entidades de seus sistema. Ou até mesmo que um requisito seja versionar todas as entidades principais. Imagine você tendo que fazer isso para todos os contextos persistentes, que mudam de uma classe para outra, pois os relacionamentos são diferentes de uma classe para outra, bem como seus tipos. Imagine manter tudo isso a cada mudança nesses relacionamentos. Vai fazer na mão?

A solução 

 Pesquisei na época que precisei disto e não encontrei nada que me desse suporte a fazer isso. Na verdade encontrei foram posts de pessoas com o mesmo problema e sempre batiam neste problemas na consistência dos relacionamentos. Mas nunca resultavam em um solução. Muito menos automatizada. Então decidi por mim mesmo fazer, na época em 2011. Comecei a pensar em cada algoritmo de replicação de acordo com cada tipo de relacionamento, de acordo com cada casa que pude vislumbrar. Então criei um "carinha" que analisa os mapeamentos com base nas anotações do javax.persistence como @Entity, @OneToMany, @OneToOne, etc... e com base nas informações de meta dados das classes das entidades envolvidas decide o que fazer e aplica a estrategia correta para gerar a replicação de cada atributo corretamente, gerando assim uma copia completa e correta de todo o contexto persistente, de toda a árvore de relacionamentos de uma entidade persistente. Esse recurso é muito fácil de usar e é transparente para qualquer um que precise.

Estou disponibilizando as classes e os fontes das mesmas de forma livre para todos que precisarem. Da mesma forma, peço favor de manter a autoria e os devidos créditos.

 Abaixo vai um exemplo e algumas instruções simples de uso. No final tem o link para baixar o componente.

Exemplo:

Pessoa pessoa = pessoaDAO.find(1);
PersistenceCloner cloner = new PersistenceCloner(pessoa); 
Pessoa pessoaCopia = cloner.generateCopyToPersist(); 

 Essas três linhas de código fazem o que queremos. Obtemos uma pessoa da base. Criamos um cloner para Pessoa. Chamamos o método generateCopyToPersist para obtermos uma copia da entidade persistente pronta se chamar o insert. Um recurso que implementei permite anotar as propriedades que serão ignorada no momento da transferência das propriedades do objeto original para a copia. O que isso quer dizer? Que podemos marcar uma propriedade que não será gerada uma copia e transferida para nova instancia criada. Digamos que nesta classe pessoa nós tenhamos um campo chamado versãoAnterior e nós não queremos que este campo seja copiado de uma versão para outra, logicamente pq cada versão apontará para uma versão anterior diferente.

Para estes casos, eu criei a anotação @NoPersistenceCopy. Essa anotação é usada para marcar propriedades que devem ser ignorada no momento da transferência das propriedades do objeto original para a copia. Ela permite que isso seja feito de duas formas. Setando simplesmente null na propriedade no objeto copia, ou forçando a copia do mesmo objeto do original para o copia mesmo que a copia seja necessária devido aos mapeamentos. 

 Exemplo1: 

 @NoPersistenceCopy 
private String versaoAnterior; 

 Isso fará com que independente do mapeamento nessa propriedade, o objeto setado nessa propriedade no objeto original no momento da copia, não passe pelo processo de copia. Ou seja, o objeto não será copiado(entenda como copia, uma nova instancia identica a original). Ao invés disso, o componente pegará o valor da propriedade no objeto original e setará como valor na mesma propriedade no objeto copia. Então a propriedade no objeto copia terá a mesma referência que a mesma propriedade no objeto original tem. 

 Exemplo2: 

@NoPersistenceCopy(setNull = true) 
private String versaoAnterior;

Isso fará com que a propriedade seja ignorada da mesma forma que foi descrita no exemplo anterior mas ao invés de setar o mesmo valor da propriedade do objeto original, o componente setará 'null' na propriedade no objeto copia. 


Resumo 

 Para utilizar, basta criar um PersistenceCloner passando a referência do objeto original que se deseja copiar, e chamar o método generateCopyToPersist. Pronto, a copia esta pronta e toda a complexidade de se fazer isto está abstraída para você.

Abaixo está o link para baixar o componente


DownloadLINK

 Qualquer dúvida, bug, sugestão ou agradecimento, postar comentário aqui os me enviar email pelo victorlindberg713@gmail.com. Abraço, espero ter ajudado.

ATUALIZAÇÃO DESSE POST EM 24/02/2023.

Pessoal, há muitos anos eu parei de evoluir o lindberg-framework. Pelo menos essa versão que continha essa solução de copia de contexto persistente.  Com o fim do google code, acabou que não levei todo o projeto pro git-hub. Mas essa parte de cópia de contexto persistente eu coloquei lá sim, mas com outro nome. Eu escrevi essa solução há mais de 10 anos atrás e até hoje pessoas passam pelo mesmo problema e sei que isso pode ajudar. Então, estou atualizando esse post aqui colocando o link atual da lib no git-hub. Abraço e espero ter ajudado a resolver o seu problema.

7 comentários:

  1. Cara, eh antigo isso o topico, mas hoje me resolveu um problemão. Valeu mesmo

    ResponderExcluir
  2. Cara, eh antigo isso o topico, mas hoje me resolveu um problemão. Valeu mesmo

    ResponderExcluir
  3. Boa Victor,
    Cara, estou na GRANDE necessidade de resolver um problema como o mencionado por você. No entanto, o link pra lib está quebrado.
    Me ajuda cara! kkkkkk

    ResponderExcluir
    Respostas
    1. Opa,,, cara. Desculpe. Faz muito tempo que não entro aqui e o email com tua mensagem aqui no post ficou perdido no limbo, então não vi a sua mensagem. Hoje, pesquisando aqui no email, foi que vi o email não lido e entrei pra ver sua mensagem. Desculpe! Na época espero que tenha conseguido resolver. Conseguiu? De qualquer forma, seu email me trouxe até aqui todos esses anos depois e com isso eu vi que o link da lib tava quebrado. Foi bom pq atualizei com o link do git hub. Se mais pessoas precisarem, agora o link ta correto. Grande abraço!

      Excluir
  4. Pessoal, depois de muito tempo sem entrar aqui, vi que o link de download da lib estava quebrado. Com o fim do google code, levei a solução pro git hub e mudei o nome. Talvez por isso algumas pessoas que precisaram não tenham encontrado. Atualizei o post com o link correto do git hub. Espero ter ajudado. Abraço!

    ResponderExcluir