Pacote de Dados

Hoje os sistemas baseados no jMine fazem uso intenso de metadados armazenados no Banco de Dados para configurar o comportamento da aplicação. Essa característica permite que o sistema possa se adaptar a novas situações sem necessitar novas versões, mas gera um grande custo de manutenção desses metadados. Tradicionalmente essas informações são migradas de uma versão para outra utilizando statements SQL, um processo trabalhoso e propenso a erros dos mais diversos.

Atualização por Pacote

Para amenizar o custo de manutenção dos metadados, além de dar mais segurança ao processo, foi criada a atualização por pacote. Um pacote é um arquivo ZIP contendo diversos arquivos que devem ser carregados no sistema, e é importado utilizando a própria tela de carga de dados. Exemplo de conteúdo:

pacote.zip
+-importar_arquivos.txt
+-security
| +-credentials.xml
+-hydra
| +-mercurius.xml
+-scripts
  +-INIT.bsh

O arquivo importar_arquivos.txt informa em que ordem, e com que drivers, os arquivos devem ser importados. Tipicamente os arquivos são scripts (.bsh, .vm) e XMLs de serviço.

Vantagens

O uso de pacote é muito útil para entidades que mudam com muita frequência e são pouco referenciadas por dados do usuário. Scripts, configurações do hydra e credenciais são os exemplos mais básicos. Ao tirar a necessidade desses alters do caminho o processo de desenvolvimento se torna mais ágil, e os scripts de alter em SQL podem se focar nas migrações estruturais mais sensíveis. Scripts de alter são muito frágeis, por serem difíceis de testar, além de sofrerem constantemente com problemas de encoding devido à manipulação indevida, portanto devem ser evitados quando não são necessários.

Limitações

Uma atualização por pacote faz todas as alterações por carga de arquivo, utilizando serviços, e não possui conhecimento sobre diferenças entre versões. Isso gera bastante simplicidade, em troca de algumas limitações:

  • Remoções não são captadas. Ex.: se um script foi removido, ele simplesmente não aparecerá no pacote e a versão antiga continuará na base após a atualização.
  • Mudanças de tipo não são permitidas. Ex.: se de uma versão para outra uma credencial de mesmo id/mnemônico passou de URLCredential para ChineseWallCredential a atualização via Hibernate não é possível.
  • O pacote é executado após a execução dos alters. Se uma migração de dados depende da existência de um elemento que seria carregado via pacote, é necessário antecipar esse registro via alter SQL. Esse registro pode inicialmente ser parcial, o mínimo para que a importação do pacote possa ler o registro e atualizá-lo. Ex.: Se um alter precisa de um script, basta adicionar a linha do script, com id, mnemônico e discriminator, os demais campos podem ser preenchidos via pacote.

Devido a essas limitações, a equipe de DBAs valida TODO o postscript após a importação, incluindo as tabelas atualizadas via pacote. Isso garante que não foi deixado lixo para trás. No dia a dia, entretanto, essas tabelas são ignoradas, pois grande parte das alterações será feita pela importação do pacote.

Geração

O pacote pode ser gerado programaticamente invocando a instância de PackCreator (disponível na Bean Factory) o método createPack() e passando para ZIP utilizando uma instância de PackToZip. Existem duas formas já implementadas de gerar o pacote:

Refdb:post

A partir da versão 1.1.7 do plugin refdb, após efetuar a carga do postscript é gerado o pacote, utilizando o procedimento acima. Em um projeto com artifactId “jmine-tec-hydra” na versão 1.9.0-SNAPSHOT será gerado no target um arquivo “jmine-tec-hydra-1.9.0-SNAPSHOT-pack.zip” com todo o postscript exportado. Esse procedimento funciona para qualquer projeto com refdb.

Diagnóstico

O pacote é gerado a partir dos dados contidos na base de dados, e apenas a partir dos dados em Postscript (id abaixo de 100.000), portanto é possível exportar o postscript de qualquer base de dados, inclusive de bases em produção, sem colocar em risco o sigilo dos dados do cliente. Os testes garantem que o pacote seja estável, portanto o postscript de um cliente pode ser gerado e comparado com o esperado utilizando qualquer ferramenta de diff. (hoje o botão que exporta o diagnóstico está disponível apenas na tela de diagnóstico em JSF, mas a adição da funcionalidade na tela em Wicket é simples)

Adicionando entidades ao pacote

Adicionar mais entidades ao pacote de importação é uma tarefa razoavelmente simples, e com teste padronizado para facilitar a implementação. Segue abaixo um passo a passo para isso:

Passo 1: Teste

Estenda AbstractPackIntegrationTest. Esse teste executa os seguintes passos:

  1. initializeTestData: prepare os dados que serão enviados para o pacote
  2. createPack: cria o pacote
  3. disruptData: corrompe os dados, removendo e modificando os dados que foram criados em initializeTestData
  4. processPack: importa o pacote gerado inicialmente
  5. inspectData: verifica que os danos feitos em disruptData foram revertidos
  6. comparePacks: cria o pacote novamente e verifica que é igual ao original

Desses é necessário apenas sobrescrever initializeTestData, disruptData e inspectData para ter um teste completo do empacotamento da sua entidade. Execute seu teste e veja que ele falha durante o inspectData. Quando chegar nesse ponto siga para o Passo 2, este é um teste de aceitação e deverá passar apenas ao fim do passo 5.

Passo 2: Exporter

Se a sua entidade não é exportável, faça ela exportável. Para isto, basta implementar ServiceFiller. Comece pelo teste, extenda AbstractEntityExporterTest. Existem inúmeros exemplos de exportadores e testes de exportadores ao longo do sistema. Quando o teste de exportação passar siga para o passo 3.

Passo 3: Serviço

Tipicamente o serviço de inclusão já existe, caso não exista escreva um teste (extenda AbstractServiceTest) que irá garantir o funcionamento do serviço. O serviço de inclusão de entidade deve utilizar o PackEntityPersistenceHelper para criar e salvar o bean. Ex.:

HydraSystem system = this.helper.findOrCreate(HydraSystem.class, this.databaseID, this.mnemonico);
system.setMnemonico(this.mnemonico);
system.setEncoding(this.encoding);
this.helper.save(system, this.databaseID);

O PackEntityPersistenceHelper deve ser injetado no serviço (@Injected). Isso fará com que o serviço tenha o comportamento de “create or update”. Caso a entidade já exista (buscando pelo id ou pelo mnemônico) ela será buscada para atualização, senão será criada. O save é feito de forma a respeitar o databaseID, mesmo para entidades já salvas.

IMPORTANTE: O PackEntityPersistenceHelper é uma interface, a implementação padrão dá prioridade ao databaseID no método findOrCreate, o que é bom para garantir que os IDs são exatamente os esperados. Existe uma implementação alternativa que dá preferência à Natural Key, esta deve ser utilizada no Refdb, para que sejam evitadas situações em que duas entidades do postscript acabem se sobrescrevendo indevidamente.

Passo 4: PopulatorDataSource

Agora que já temos testados e funcionando o serviço e o exportador, podemos passar para o empacotamento.

public interface PopulatorDataSource<T> {
         Iterable<PopulatorBean<T>> load();
}

Implemente um PopulatorDataSource, de preferência extendendo AbstractPopulatorDataSource. Esse objeto deve retornar um ou mais PopulatorBeans. Um PopulatorBean possui um objeto a ser empacotado e o nome do arquivo de destino. O objeto será “serializado” no pacote utilizando o exportador, gerando um XML de serviços. Ex.: O código abaixo lê todos os HydraSystems e, para cada um, cria um arquivo em hydra/<mnemônico sistema>.xml.

public class HydraPopulatorDataSource extends AbstractPopulatorDataSource<HydraSystem> {

         private HydraSystemDAO hydraSystemDAO;

         public Iterable<PopulatorBean<HydraSystem>> load() {
             return MapperUtils.mapList(this.hydraSystemDAO.findAllOrderedByPk(), new Mapper<HydraSystem, PopulatorBean<HydraSystem>>() {
                 public PopulatorBean<HydraSystem> apply(HydraSystem input) {
                     return new PopulatorBean<HydraSystem>(input, "hydra", input.getMnemonico().toLowerCase() + ".xml");
                 }
             });
         }
}

Passo 5: Registro no Spring

Declare seu PopulatorDataSource e crie um PackPopulator, utilizando como parent o abstractServicePackPopulator. Isso irá gerar um populador de pacote de arquivos que exporta a entidade utilizando XML de serviços, criado a partir do exportador implementado no passo 2 e que utilizará o serviço do passo 3. Caso a sua entidade dependa de outras, é necessário listar as dependências como é feito no exemplo abaixo. As entidades do Hydra dependem que os scripts já tenham sido carregados anteriormente, portanto é registrado que primeiro os scripts devem ser empacotados, depois as entidades do hydra.

<bean id="hydraPopulatorDataSource" class="jmine.tec.hydra.pack.HydraPopulatorDataSource">
        <property name="hydraSystemDAO" ref="hydraSystemDAO" />
</bean>

<bean id="hydraPackPopulator" parent="abstractServicePackPopulator">
        <property name="dataSource" ref="hydraPopulatorDataSource" />
</bean>

<bean id="registrarHydraPackPopulator" parent="abstractPackPopulatorRegistry">
        <property name="registers">
                <list>
                        <ref bean="beanshellScriptPackPopulator" />
                        <ref bean="velocityScriptPackPopulator" />
                        <ref bean="hydraPackPopulator" />
                </list>
        </property>
</bean>

Nesse momento o teste feito no Passo 1 deverá passar. Execute o plugin refdb:post em algum projeto que tenha o postscript do módulo e procure no diretório target um arquivo terminado em “pack.zip”, abra e veja o resultado da sua implementação.

Entidades com associações

Para entidades que possuem relações não bi-direcionais, ou seja, em que uma entidade A possui referência para uma entidade B, mas B não possui referência para a entidade A, é necessário uma implementação que represente a associação existente entre A (entidade base) e B (entidade associada). Para isso, basta utilizar uma instância concreta que extenda AbstractImplicitAssociation que representará a associação entre as entidades A e B.

EX.: A entidade Produto tem referência para a entidade Classificacao, mas Classificacao não tem referência para Produto, dessa forma foi implementada a classe ProdutoClassificacaoAssociation que extende AbstractImplicitAssociation, e define que Produto é a entidade base e tem referência para Classificacao, a entidade associada, como mostra o código abaixo:

public class ProdutoClassificacaoAssociation extends AbstractImplicitAssociation<Produto, Classificacao> {

         /**
          * Constructor.
          *
          * @param baseEntity {@link Produto}
          * @param associatedEntity {@link Classificacao}
          */
         public ProdutoClassificacaoAssociation(Produto baseEntity, Classificacao associatedEntity) {
             super(baseEntity, associatedEntity);
         }
}

Após a implementação que estende AbstractImplicitAssociation, basta, caso não exista, criar um serviço que implemente ServiceFiller e um exporter desse serviço. É importante ressaltar que no teste de pacote da entidade base dessa associação (no exemplo acima, ProdutoPackIntegrationTest, que é a entidade base na associação Produto x Classificação), as associações feitas também devem ser testadas nos métodos disruptData() e inspectData().