Novembro de 2017

Volume 33 - Número 11

DevOps – Migração de dados contínua com o Visual Studio e o TFS

Por Jebarson Jebamony

A migração de dados geralmente é um requisito para o desenvolvimento de aplicativos, seja em projetos de criação sem restrições ou na reformulação de um aplicativo existente. Apesar disso, a migração de dados praticamente não é levada em conta durante as fases de design e desenvolvimento, e só é considerada na etapa final do projeto. Embora essa abordagem permita que a etapa de migração seja mais concentrada, ela oferece alguns riscos, e o mais grave deles é as equipes não terem o tempo e os recursos adequados para testar apropriadamente a migração e os códigos adjacentes.

O conceito de migração de dados contínua é baseado na ideia de desenvolver scripts de migração durante o desenvolvimento de um aplicativo e mantê-los em todas as versões subsequentes do aplicativo, como se eles fizessem parte do código. A abordagem da migração contínua permite que você teste suas migrações junto com o desenvolvimento do código, assegurando que os dados e os códigos estejam sempre sincronizados.

Neste artigo, descrevo uma solução que utiliza o Visual Studio e o Team Foundation Server para a migração de dados contínua. Lembre-se de que existem ferramentas de terceiros, como o Red Gate Ready Roll, capazes de executar parte do processo, mas elas costumam ser caras e não permitem a migração de dados contínua.

Desafio e solução

O Visual Studio pode executar a publicação incremental de um banco de dados com a ajuda do SqlPackage.exe, mas essa ferramenta não oferece todos os recursos necessários. Por exemplo, o SqlPackage.exe não permite inserir uma nova coluna em uma tabela, alterar os dados de propagação, normalizar e desnormalizar tabelas, entre outras coisas.

Além disso, as alterações no controle de versão são muito importantes quando você precisa fazer implantações e correções segmentadas. Por exemplo, talvez você precise aumentar o valor de uma coluna em 10 quando migrar da v1.2 para a v1.3, mas isso pode não ser necessário qualquer em outro fluxo. Só é possível fazer isso com o controle de versão, mas o SQL não oferece essa possibilidade.

Quero superar esse desafio criando uma solução que aproveite ao máximo o que o Visual Studio e o SqlPackage.exe podem oferecer, enquanto resolvo as limitações que mencionei antes.

Um projeto de banco de dados típico tem dois tipos de script: compilados e não compilados. Todos os objetos, como o esquema, as tabelas, as exibições, os procedimentos armazenados, etc., geralmente são escritos como um script compilado. O script de propagação e todas as consultas de tempo de execução geralmente serão adicionados ao script pós-implantação não compilado.

Vamos começar com um exemplo. A Figura 1 mostra o projeto de banco de dados de exemplo Adventure­Works.Database (importado do backup disponível em bit.ly/2vPwu4N). Como você pode ver, todos os objetos são adicionados aos scripts compilados.

Scripts compilados no AdventureWorks.Database

Figura 1: Scripts compilados no AdventureWorks.Database

O script de dados de propagação (que contém os dados necessários para o aplicativo funcionar) são adicionados a um script não compilado e referenciados no script pós-implantação. A Figura 2 mostra isso. Se você não souber o que são scripts pós-implantação, aconselho ler a documentação na biblioteca da MSDN bit.ly/2w12Iy4.

Scripts pós-implantação

Figura 2: Scripts pós-implantação

Para garantir que um script pós-implantação seja capaz de realizar uma implantação incremental, adicionei uma cláusula NOT EXISTS antes de todas as instruções INSERT. Por exemplo:

IF NOT EXISTS (SELECT 1 FROM [Person].[AddressType] WHERE [AddressTypeID] = 1)
INSERT [Person].[AddressType] ([AddressTypeID], [Name], [ModifiedDate]) VALUES (1 N’Billing’, CAST (N’2008-04-30T00:00.000’ AS DateTime))

Por uma questão de simplicidade e para facilitar a manutenção, manterei todos os scripts de propagação em seus respectivos arquivos e vou referenciá-los no script pós-implantação.

Agora tenho um projeto que implantará o esquema mais recente e propagará dados a qualquer hora. Ele também é capaz de realizar uma implantação incremental em um banco de dados existente se o projeto não incluir alterações que causem falhas. No entanto, as restrições que mencionei no início desta seção começam a surgir.

Por fim, há um bug que interrompe a implantação incremental quando um tipo definido pelo usuário (UDT) é alterado. Infelizmente, a equipe do Visual Studio marcou esse bug como sem correção, o que significa que você precisará corrigi-lo sozinho. Para obter mais informações sobre o bug na comunidade de desenvolvedores do Visual Studio, acesse bit.ly/2w0zTBU.

Controle de versão

Assim como você controla a versão de todos os aplicativos que desenvolve, também é importante controlar a versão do banco de dados. O controle de versão ajuda você a rastrear o código-fonte para monitorar facilmente os recursos, os bugs e as correções feitas em cada versão do software. Se você não estiver familiarizado com o controle de versão, reserve um tempo para ler o artigo “Versionamento Semântico 2.0.0” em semver.org. Vale a leitura.

Antes de começar, tenho que superar um desafio: o SQL não tem propriamente um mecanismo de controle de versão, por isso preciso criar um. Criarei uma tabela chamada [internal].[Database­Version] para armazenar os detalhes da versão, onde “internal” é o esquema da tabela. É uma boa medida ter um esquema separado para todos os objetos do banco de dados de uso interno (ou seja, eles não participam de fato das execuções).

A Figura 3 mostra a minha proposta de esquema para a tabela. Você pode seguir seu próprio padrão se preferir, mas lembre-se de que criamos versões para monitorar compilações e liberações.

Figura 3: Esquema da tabela

CREATE TABLE [internal].[DatabaseVersion]
(
 [DatabaseVersionId] INT IDENTITY(1,1) NOT NULL,
 [Major] INT NOT NULL,
 [Minor] INT NOT NULL,
 [Build] INT NOT NULL,
 [Revision] INT NOT NULL,
 [CreatedBy] NVARCHAR (256) 
CONSTRAINT [DFDatabaseVersionCreatedBy] DEFAULT ('') NOT NULL,
 [CreatedOn] DATETIME 
CONSTRAINT [DFDatabaseVersionCreatedOn] DEFAULT (GETUTCDATE()) NOT NULL,
 [ModifiedBy] NVARCHAR (256) 
CONSTRAINT [DFDatabaseVersionModifiedBy] DEFAULT ('') NOT NULL,
 [ModifiedOn] DATETIME 
CONSTRAINT [DFDatabaseVersionModifiedOn] DEFAULT (GETUTCDATE()) NOT NULL,
 CONSTRAINT [PKDatabaseVersion] PRIMARY KEY CLUSTERED ([DatabaseVersionId] ASC)
);GO

Sempre que eu fizer uma alteração no esquema ou adicionar um script de migração de dados, adicionarei uma nova entrada de versão à tabela, que serve de rótulo da alteração. Se a versão atual for 1.0.0.0, e eu adicionar uma migração que corrija o envio do sinalizador Gender por meio da reversão dos valores, adicionarei os scripts apropriados para executar essa alteração e uma nova entrada à tabela com a versão, por exemplo, 1.1.0128.212.

Migração

Como eu já disse antes, o Visual Studio pode executar implantações incrementais, mas sem alterações que causem falhas. Por isso, quando eu elaborar a migração, preciso consideração isso e solucionar essa limitação.

A primeira etapa é criar um projeto separado para a migração. Com o exemplo na Figura 3, criei um projeto de banco de dados chamado AdventureWorks.Database.Migration. Esse projeto de migração tem dois tipos de script. O primeiro é o script da migração de dados que precisa ser executado se qualquer movimentação ou atualização de dados ocorrer. O segundo script é responsável pelas alterações que causam falhas no esquema que o Visual Studio e o SqlPackage.exe não podem corrigir. Esses dois scripts são adicionados ao projeto como um script pós-implantação. Não existem scripts compiláveis neste projeto.

Para entender melhor o cenário, vamos discutir todos os aspectos do exemplo AdventureWorks. Carreguei o código-fonte desse exemplo no meu repositório do GIT em github.com/Jebarson/ContinuousDataMigration. A ramificação mestra incorpora o projeto base que importei e criei a partir do bando de dados, conforme mencionei anteriormente.

Antes de analisar o cenário, quero explicar como a migração funciona. Como descrevi na seção Controle de versão, estou criando um controle de versão para cada alteração liberada adicionando uma nova linha a internal.DatabaseVersion. Dentro do projeto AdventureWorks.Database.Mi­gration, escrevi a lógica para executar os scripts de migração apropriados com base na versão do banco de dados pretendida. Veja o fluxograma na Figura 4 para entender a lógica do processo.

A lógica da migração

Figura 4: A lógica da migração

No início do projeto AdventureWorks.Database.Migration, verifico a versão atual do banco de dados e, com base nela, executo os scripts de migração até a versão mais recente. Este é o trecho de código que utilizo para definir o caminho da migração, que chamarei de Script 1:

DECLARE @currentDBVersion BIGINT = NULL;

-- Get the current version of the database.
SELECT TOP 1 @currentDBVersion = Id FROM [internal].[DatabaseVersion] ORDER BY [DatabaseVersionId] DESC

-- Jump to the incremental migration scripts based on the current version.
IF @currentDBVersion = 1 GOTO Version11
ELSE IF @currentDBVersion = 2 GOTO Version12
ELSE
RETURN

Agora que mostrei como os scripts de migração são executados, vamos realizar a migração com alguns cenários fictícios que ajudam a ilustrar o que está acontecendo. Mostrarei duas mudanças de versão no projeto base que criai antes.

Versão 1.1: esta é a primeira mudança no projeto base que criei. As alterações estão disponíveis na ramificação v11 branch do projeto Continuous Data Migration (Migração de dados contínua) no GitHub. As alterações que fiz nesta versão são as seguintes:

  • Adicionei uma nova coluna IsEmployee em [HumanResources].[Em­ployee] depois da coluna JobTitle.
  • Alterei o nome em [Person].[AddressType] de Main Office para Office.
  • Alterei os SPs (não é necessário incluir isso no projeto de migração).
  • Novos SPs (não é necessário incluir isso no projeto de migração).

Todas essas alterações foram feitas no projeto base AdventureWorks.Database original, junto com a linha da nova versão no internal.DatabaseVersion. Isso facilita a incorporação das alterações mais recentes a qualquer nova implantação. Para os bancos de dados existentes, cuja versão original será atualizada para a versão v1.1, preciso implementar as mesmas alterações no projeto de migração. Então, dividirei esse processo em duas partes: a alteração do esquema e a alteração dos dados. Inserir uma nova coluna IsEmployee é uma alteração no esquema, e alterar AddressType de Main Office para Office é uma alteração nos dados.

O Visual Studio pode fazer uma alteração no esquema. No entanto, ele só pode incluir a coluna, e isso é algo que não quero. Para superar essa limitação, preciso gerar um script para primeiro eliminar todas as dependências (índices, restrições, chaves estrangeiras e elementos semelhantes) da tabela Employee. Depois, criarei uma tabela temporária com a nova coluna na ordem correta e com todas as dependências. Agora, posso mover os dados da tabela Employee para a tabela temporária, excluir a tabela Employee e, por fim, renomear a tabela temporária como Employee. Este script está disponível na ramificação v11 do meu projeto Continuous Data Migration (Migração de dados contínua) no GitHub, no arquivo SchemaChangeScript.sql.

A alteração dos dados muda apenas o valor Main Office para Office. Por isso, posso adicionar ao script uma consulta de atualização que faça isso. Consulte o arquivo DataChangeScript.sql na ramificação v11 do projeto Continuous Data Migration (Migração de dados contínua) no GitHub.

Quando o projeto de migração for executado no AdventureWorks.Database existente, o código do Script 1 enviará a execução para um script que chama os scripts de alteração do esquema e dos dados, que chamarei de Script 2, como mostra o trecho de código abaixo:

-- Script to migrate to v1.1
Version11:
:r .\Scripts\Migration\V11\SchemaChangeScript.sql
:r .\Scripts\Migration\V11\DataChangeScript.sql

EXEC [internal].[CreateDatabaseVersion] @id = 2, @major = 1, @minor = 1, 
 @build = 0128, 
 @revision = 212

Versão 1.2: Esta é a alteração mais recente executada depois de v1.1. As mesmas alterações estão disponíveis na ramificação v12 do projeto no GitHub. As alterações nesta versão são as seguintes:

  • Alterei IsEmployee em [HumanResources].[Employee] para EmployeeType, referenciando uma nova tabela para [HumanResources].[EmployeeType].
  • Alterei os SPs (não é necessário incluir isso no projeto de migração).
  • Nova tabela (não é necessário incluir isso no projeto de migração).

Semelhante a v1.1, também fiz alterações no projeto AdventureWorks.Database original junto com uma nova entrada em internal.DatabaseVersion. Como podemos ver, IsEmployee foi alterado para EmployeeType, a fim de acomodar mais tipos de funcionários. Para isso, segui o mesmo padrão que utilizei em v1.1. No entanto, preciso escrever a migração de dados para a nova coluna, com base no valor da coluna anterior. O script da alteração do esquema está no arquivo SchemaChangeScript.sql na ramificação v12 do meu projeto Continuous Data Migration (Migração de dados contínua) no GitHub.

Este é o script que adicionei ao projeto para migrar para v1.2, e que chamarei de Script 3:

-- Script to migrate to v1.2
Version12:
:r .\Scripts\Migration\V12\SchemaChangeScript.sql

EXEC [internal].[CreateDatabaseVersion] @id = 3, @major = 1, @minor = 2, 
 @build = 0414, 
 @revision = 096

Conforme mencionei antes, o Visual Studio é parcialmente capaz de executar uma implantação incremental, mas falarei sobre os itens que o Visual Studio não dispõe com os scripts que gerei até agora. Você deve ter percebido que adicionei a observação “não é necessário incluir isso no projeto de migração” para alguns dos itens de v1.1 e v1.2. Fiz isso, porque o Visual Studio é capaz implantá-los incrementalmente. Então, fica a pergunta: quais alterações podem ser executadas com um projeto de migração e quais não podem?

Você pode consultar a tabela na Figura 5, para decidir se é melhor usar um script de migração ou não. Observe que você poderá se deparar com outros itens que poderiam estar nessa lista.

Alterar Triagem
Nova tabela/exibição/procedimento armazenado/objeto Aproveite o Visual Studio
Alteração de exbição/procedimento armazenado/função Aproveite o Visual Studio
Alteração no tipo definido pelo usuário Elimine todos os procedimentos armazenados do UDT. Essa é uma solução para o bug descrito anteriormente.
Adição de uma nova coluna à tabela Crie um script de migração da tabela existente para uma nova tabela incluindo a ordem correta das colunas (consulte github.com/Jebarson/ContinuousDataMigration). Você não precisará fazer isso se estiver adicionando uma coluna anulável e se a posição da coluna não for importante.
Normalização ou desnormalização de tabela Crie um script de migração para dividir ou mesclar de acordo com a necessidade. Esse script é semelhante aquele criado em v1.2.
Alteração nos dados Crie um script para alterar os dados.

 

Figura 5: Lista de itens do projeto de migração

Bem, já falei bastante sobre a geração de scripts de migração. É hora de prosseguir para a implantação.

Para implantar uma nova instância da versão mais recente de um banco de dados, não é necessário fazer uma migração. No exemplo que estou usando aqui, tudo o que você precisa implantar é o AdventureWorks.Database. Para fazer isso, você pode usar o Visual Studio (via publicação) ou o SqlPackage.exe. Veja abaixo o comando para implantar o banco de dados usando o SqlPackage.exe:

SqlPackage.exe /Action:Publish /SourceFile:"AdventureWorks.Database.dacpac" /tdn:<<DatabaseName>> /tsn:"<<SQL Instance>>"

Quando você executa uma implantação incremental em um banco de dados existente, talvez o script mais recente precise de uma migração. Isso significa que precisarei implantar o banco de dados da migração também. Farei isso implantando o projeto AdventureWorks.Database.Migration primeiro, e o AdventureWorks.Database depois. Certifique-se de que a opção “Sempre recriar banco de dados” esteja desmarcada na área Opções Avançadas de Implantação da caixa de diálogo Configurações de Publicação Avançadas, como mostra a Figura 6.

A caixa de diálogo Configurações de Publicação Avançadas

Figura 6: A caixa de diálogo Configurações de Publicação Avançadas

SqlPackage.exe /Action:Publish /SourceFile:"AdventureWorks.Migration.Database.dacpac" /tdn:<<DatabaseName>> /tsn:"<<SQL Instance>>" /p:CreateNewDatabase = False
SqlPackage.exe /Action:Publish /SourceFile:"AdventureWorks.Database.dacpac" /tdn:<<DatabaseName>> /tsn:"<<SQL Instance>>" /p:CreateNewDatabase = False

Três problemas comuns da migração e suas respectivas correções

A migração de dados contínua pode oferecer muitos benefícios, mas também apresenta desafios. Falarei sobre alguns problemas comuns que você pode enfrentar durante a implementação dessa solução e, claro, como corrigi-los.

Você pode receber a mensagem de erro mostrada abaixo no projeto de migração, caso sua versão recente de um script de migração tenha removido um objeto que seja referenciado em uma versão anterior desse script. A solução para isso é escrever as consultas no seguinte formato: sp_executesql ‘<<seu script de migração>>.’ Por exemplo:

EXEC sp_executesql 'ALTER TABLE Employee ADD NewCol INT'

Scripts de migração fora de controle e excesso de versões:

É sempre bom definir uma versão de destino mínima para migração. Isso limita o escopo dos scripts de migração e ajuda a garantir que eles não se tornem difíceis de manter.

Implementação com um banco de dados de produção:

Caso você queira implementar essa solução em um banco de dados que já esteja em uso na produção, inclua a definição de internal.Database­Version e suas entradas de versão. Altere o “Script 1” para verificar se a tabela internal.DatabaseVersion existe. Se ela não existir, direcione a execução para o rótulo da versão mais recente, que executará a migração e também criará a tabela. Por exemplo:

DECLARE @currentDBVersion BIGINT = NULL;

-- Get the current version of the database.
IF NOT EXISTS(SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE [TABLES].
[TABLE_NAME]='DatabaseVersion' AND [TABLES].[TABLE_SCHEMA]='internal')
SELECT @currentDBVersion = 1
ELSE
SELECT TOP 1 @currentDBVersion = Id FROM [internal].[DatabaseVersion]
ORDER BY [DatabaseVersionId] DESC
-- Jump to the incremental migration scripts based on the current version.
IF @currentDBVersion = 1 GOTO Version11
ELSE IF @currentDBVersion = 2 GOTO Version12
ELSE
RETURN

Configuração do TFS Build para implantar a migração contínua

O objetivo é automatizar as migrações exatamente como a integração contínua, para que o servidor da compilação faça a migração dos dados e os disponibilize para desenvolvedores e testadores assim que a compilação for disparada. A próxima etapa é configurar as tarefas de liberação da compilação.

Para criar tarefas para o compilação, primeiro você precisa saber como criar uma compilação de integração contínua. Se não souber, consulte o tutorial publicado no site do Microsoft Visual Studio em bit.ly/2xWqtUx.

Depois de criar as tarefas de compilação, você precisa criar as tarefas de implantação para o banco de dados. Neste exemplo, você precisará adicionar duas tarefas de implantação: uma para o projeto AdventureWorks.Database.Migration e outra para o projeto AdventureWorks.Database. A tarefa de implantação será algo parecido com a Figura 7.

A tarefa de implantação

Figura 7: A tarefa de implantação

Forneça as informações e configure o gatilho com base no seu requisito. Assim que a compilação estiver operacional, você precisará configurar uma migração de dados contínua para seu aplicativo.

Conclusão

Neste artigo, expliquei a importância da migração de dados contínua em um projeto que envolve várias fases de liberação e como fazer isso usando o Visual Studio e o TFS. A migração de dados contínua ajuda a reduzir os esforços de desenvolvimento e os bugs da migração. Pessoalmente, já reduzi em 40% os esforços de migração na etapa de desenvolvimento. Ela também dispensa a fase de migração do projeto.

A integração dos scripts de migração no TFS é tão importante quanto os próprios scripts de migração. O processo de migração de dados contínua não terá utilidade alguma se ele não for implantado como parte de uma compilação diária. “Detecte as falhas rapidamente” é o mantra do desenvolvimento de software, e a migração de dados contínua ajuda você a fazer isso.


Jebarson Jebamony é consultor sênior dos serviços da Microsoft, bem como desenvolve soluções para a Microsoft, seus parceiros e clientes. Ele tem mais de 14 anos de experiência técnica e trabalhou com várias tecnologias da Microsoft nesse período.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Sagar Dheram e Shrenik Jhaveri


Discuta esse artigo no fórum do MSDN Magazine