Compartir a través de



Noviembre de 2017

Volumen 33, número 11

DevOps: migración de datos continua con Visual Studio y TFS

Por Jebarson Jebamony

La migración de datos suele ser un requisito en un esfuerzo de desarrollo de aplicaciones, tanto si se trata de un proyecto totalmente nuevo como del rediseño de una aplicación. A pesar de esto, se presta muy poca atención a la migración de datos durante la fase de diseño y desarrollo, ya que la actividad suele dejarse para la última parte del proyecto. Aunque esta estrategia podría permitir un enfoque íntegro sobre la migración, presenta algunos riesgos, como la falta de tiempo y recursos de los equipos para probar correctamente la migración y el código circundante.

El concepto de migración de datos continua se basa en la idea de desarrollar scripts de migración durante el desarrollo de una aplicación y su mantenimiento entre versiones, igual que el código de la aplicación. Un enfoque de migración continua permite probar las migraciones durante el desarrollo de código, lo que garantiza la sincronización de los datos y los activos de código.

En este artículo, describo una solución que usa Visual Studio y Team Foundation Server para conseguir la migración de datos continua. Tenga en cuenta que existen herramientas de terceros, como ReadyRoll de RedGate, que son capaces de hacerlo parcialmente, pero tienen un costo elevadísimo y carecen de la capacidad de migración de datos continua.

Desafío y solución

Visual Studio puede realizar una publicación incremental de una base de datos con la ayuda de SqlPackage.exe, pero la herramienta es insuficiente en muchos aspectos. Por ejemplo, SqlPackage.exe fracasa al insertar una nueva columna entre las columnas de una tabla, cambiar los datos semilla, y normalizar y desnormalizar tablas, entre otros ejemplos.

Asimismo, los cambios de versiones son muy importantes cuando se tienen que realizar implementaciones y correcciones dirigidas. Por ejemplo, es posible que deba incrementar el valor de una columna en 10 al migrar de v1.2 a v1.3, pero no en ningún otro flujo. Esto solo se puede conseguir con el control de versiones; no obstante, SQL carece de esta funcionalidad.

Quiero abordar el desafío mediante el diseño de una solución que aproveche todo lo que Visual Studio y SqlPackage.exe pueden ofrecer, y resolver al mismo tiempo las carencias mencionadas anteriormente.

Un proyecto de base de datos típico tiene dos tipos de scripts: compilados y no compilados. Todos los objetos, como esquemas, tablas, vistas, procedimientos almacenados, etc., se escriben por lo general como un script compilado. El script semilla y todas las consultas en tiempo de ejecución se colocarán por lo general en el script posterior a la implementación, que no está compilado.

Empecemos con un ejemplo. En la Figura 1 se muestra el proyecto de base de datos de ejemplo Adventure­Works.Database (importado de la copia de seguridad disponible en bit.ly/2vPwu4N). Como puede ver, todos los objetos están colocados como scripts compilados.

Scripts compilados en AdventureWorks.Database

Figura 1 Scripts compilados en AdventureWorks.Database

El script de datos semilla (que contiene los datos necesarios para que la aplicación funcione) está colocado como un script no compilado y se menciona en el script posterior a la implementación. Esto se muestra en la Figura 2. Si desconoce la importancia de los scripts posteriores a la implementación, le recomiendo que lea la documentación de MSDN Library en bit.ly/2w12Iy4.

Scripts posteriores a la implementación

Figura 2 Scripts posteriores a la implementación

Para garantizar que el script posterior a la implementación puede controlar la implementación incremental, he agregado una cláusula NOT EXISTS delante de todas las instrucciones INSERT. Por ejemplo:

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))

Para favorecer la simplicidad y la facilidad de mantenimiento, mantendré todos los scripts semilla en sus respectivos archivos y haré referencia a estos nuevamente en el script posterior a la implementación.

Ahora tengo un proyecto que implementará el esquema y los datos semilla más recientes, en cualquier momento. También es capaz de realizar una implementación incremental en una base de datos existente si el proyecto no introduce cambios de última hora. No obstante, entran en juego las restricciones que mencioné al principio de esta sección.

Finalmente, existe un error que interrumpe la implementación incremental cuando se cambia un tipo definido por el usuario (UDT). Lamentablemente, el equipo de Visual Studio ha marcado este error para que no se corrija, lo que significa que tendrá que evitarlo. Puede explorar más detalles sobre el error en la entrada de Visual Studio Developer Community en bit.ly/2w0zTBU.

Versiones

Del mismo modo que controla las versiones de todas las aplicaciones que envía, es importante hacerlo también con la base de datos. El control de versiones le ayuda a realizar un seguimiento del código fuente, lo que le permitirá mantenerse al día sobre las características, los errores y las correcciones de cada versión del software. Si todavía no está familiarizado con el control de versiones, dedique un momento a consultar el artículo "Versionado Semántico 2.0.0-rc.2" en semver.org. Vale la pena leerlo.

Antes de empezar, debo abordar un desafío: SQL no incluye realmente un mecanismo de creación de versiones, así que tendré que crear uno propio. Crearé una tabla denominada [internal].[Database­Version] para almacenar los detalles de la versión, donde "internal" es el esquema de la tabla. Se recomienda tener un esquema independiente para todos los objetos de base de datos usados con fines internos (es decir, que no participan en el negocio real).

En la Figura 3 se muestra el esquema que yo propondría para la tabla. Puede seguir su propio patrón si este no le convence. Tenga en cuenta que creamos versiones para mantener un registro de las compilaciones y las versiones.

Figura 3 Esquema de tabla

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

Cada vez que realizo un cambio en el esquema o inserto en el repositorio un script de migración de datos, agrego una entrada de versión nueva a la tabla, que sirve de etiqueta del cambio. Si la versión actual es 1.0.0.0 e introduzco una migración que corrige el problema de la marca de Sexo mediante la inversión de los valores, agregaré scripts adecuados para realizar ese cambio y agregar una nueva entrada a la tabla con la versión 1.1.0128.212.

Migración

Como ya expliqué anteriormente, Visual Studio puede realizar implementaciones incrementales, pero no con cambios de última hora. Por tanto, mientras diseño la migración, debo tenerlo en cuenta y evitar la limitación.

El primer paso es crear un proyecto independiente para dirigir la migración. En relación con el ejemplo de la Figura 3, creo un nuevo proyecto de base de datos denominado AdventureWorks.Database.Migration. Este proyecto de migración dirige dos tipos de scripts. El primero es el script de migración de datos, que debe ejecutarse si se produce cualquier movimiento o actualización de datos. El segundo script se ocupa de los cambios de última hora en el esquema que Visual Studio y SqlPackage.exe no pueden controlar. Ambos scripts se incluyen en el proyecto como un script posterior a la implementación. No existen scripts compilables en este proyecto.

Para comprender mejor el escenario, vamos a explicar todo lo relacionado con el ejemplo de AdventureWorks. He cargado el código fuente correspondiente en mi repositorio GIT en github.com/Jebarson/ContinuousDataMigration. La rama maestra incorpora el proyecto base que he importado y creado a partir de la base de datos, como mencioné anteriormente.

Antes de adentrarme en este escenario, me gustaría explicar cómo funciona la migración. Como traté en la sección Versiones, controlo las versiones de todos los cambios, para lo cual agrego una nueva fila en internal.DatabaseVersion. Dentro del proyecto AdventureWorks.Database.Mi­gration, escribo la lógica para ejecutar scripts de migración adecuados en función de la versión de base de datos de destino. Consulte el diagrama de flujo de la Figura 4 para comprender la lógica implicada.

Lógica de migración

Figura 4 Lógica de migración

Al principio del proyecto AdventureWorks.Database.Migration, compruebo la versión actual de la base de datos y según cuál sea, ejecuto scripts de migración hasta la versión más reciente. A continuación, incluyo el fragmento de código que uso para describir la ruta de migración, que denominaré 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

Ahora que ya he mostrado cómo se ejecutan los scripts de migración, vamos a dirigir la migración con algunos escenarios ficticios que nos ayudarán a ilustrar qué sucede. Explicaré dos cambios de versión del proyecto base que creé anteriormente.

Versión 1.1: este es el primer cambio sobre el proyecto base que creé. Los cambios están disponibles en la rama v11 del proyecto de migración de datos continua en GitHub. Los cambios que confirmé en esta versión son los siguientes:

  • Inserté una nueva columna IsEmployee para [HumanResources].[Em­ployee] después de la columna JobTitle.
  • Cambié el nombre de [Person].[AddressType] de Main Office a Office.
  • SP modificados (no es necesaria su inclusión en el proyecto de migración).
  • SP nuevos (no es necesaria su inclusión en el proyecto de migración).

Todos estos cambios se realizan en el proyecto regular AdventureWorks.Database tal cual, junto con la nueva fila de versión en internal.DatabaseVersion. Esto permite que cualquier nueva implementación incorpore fácilmente los cambios más recientes. Para cualquier base de datos existente cuya versión de base deba actualizarse a v1.1, debo implementar los mismos cambios en el proyecto de migración. Para ello, lo segrego en dos secciones: el cambio de esquema y el cambio de datos. La inserción de una nueva columna denominada IsEmployee es un cambio de esquema, mientras que la modificación de AddressType de Main Office a Office es un cambio de datos.

El cambio de esquema es algo que Visual Studio puede hacer. No obstante, solo puede anexar la columna, que es algo que no quiero. Para superar esta limitación, debo generar un script para colocar primero todas las dependencias (índices, restricciones, claves externas y similares) de la tabla Employee y, luego, crear una tabla temporal con la nueva columna en el orden correcto y con todas las dependencias. A continuación, puedo mover los datos de la tabla Employee a la tabla temporal, colocar la tabla Employee y, por último, cambiar el nombre de la tabla temporal por Employee. Este script está disponible en la rama v11 de mi proyecto de migración de datos continua en GitHub, en el archivo SchemaChangeScript.sql.

El cambio de datos solo altera un valor del registro de Main Office a Office y, por tanto, puedo escribir una consulta de actualización para conseguirlo. Consulte el archivo DataChangeScript.sql de la rama v11 del proyecto de migración de datos continua en GitHub.

Si el proyecto de migración se ejecuta sobre el proyecto AdventureWorks.Database existente, el código de Script 1 enviará la ejecución a un script que llame al script de cambio de esquema y de datos, como se muestra en el fragmento de código siguiente, que denominaré Script 2:

-- 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

Versión 1.2: este es el último cambio confirmado sobre v1.1. Los mismos cambios están disponibles en la rama v12 del proyecto en GitHub. Los cambios de esta versión son los siguientes:

  • Cambié IsEmployee en [HumanResources].[Employee] por EmployeeType, de modo que la nueva tabla se remitió a [HumanResources].[EmployeeType].
  • SP modificados (no es necesaria su inclusión en el proyecto de migración).
  • Tabla nueva (no es necesaria su inclusión en el proyecto de migración).

Prácticamente como en v1.1, también realicé los cambios en el proyecto regular AdventureWorks.Database junto con una nueva entrada en internal.DatabaseVersion. Como puede ver, IsEmployee ha cambiado a EmployeeType para admitir más tipos de empleados. Para conseguirlo, sigo el mismo patrón que usé en v1.1. No obstante, debo escribir la migración de datos de la nueva columna de acuerdo con el valor de la columna anterior. El script de cambio de esquema está escrito en el archivo SchemaChangeScript.sql en la rama v12 de mi proyecto de migración de datos continua en GitHub.

El siguiente es el script que he incluido en el proyecto para la migración a v1.2, que denominaré 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

Como mencioné anteriormente, Visual Studio puede dirigir una implementación parcialmente, pero solo me centro en los elementos que faltan en Visual Studio con los scripts que he generado hasta el momento. Es posible que haya notado que, para determinados elementos tanto de v1.1 como de v1.2, he indicado que "no es necesaria su inclusión en el proyecto de migración". Eso se debe a que Visual Studio es capaz de implementarlos de manera incremental. Entonces, la pregunta es la siguiente: ¿Cuáles son los cambios aptos para un proyecto de migración y cuáles no?

Puede consultar la práctica lista de referencia de la Figura 5, que le ayudará a decidir si escribe la migración o no. Tenga en cuenta que podría encontrarse con otros elementos que podrían agregarse a esta lista.

Cambiar Evaluación de prioridades
Tabla, vista, procedimiento almacenado u objeto nuevo Usar Visual Studio.
Cambio en una vista, un procedimiento almacenado o una función Usar Visual Studio.
Cambio en el tipo definido por el usuario Quitar todos los procedimientos almacenados relacionados del UDT. Esta es una solución para el error descrito anteriormente.
Adición de una nueva columna a la tabla Generar scripts de una migración de la tabla existente a una nueva tabla con el orden de columnas correcto (consulte github.com/Jebarson/ContinuousDataMigration). Esto no es necesario si está agregando una columna que acepte valores NULL y el orden de la columna es irrelevante.
Normalización o desnormalización de una tabla Generar scripts de una migración para la división o combinación en función de las necesidades. Es similar al script creado en v1.2.
Cambio en los datos Generar scripts del cambio de los datos.

 

Figura 5 Lista de referencia del proyecto de migración

Bien, la información sobre la generación de los scripts de migración es suficiente. Es hora de pasar a la implementación.

Al implementar una nueva instancia de la última versión de una base de datos existente, no es necesario realizar ninguna migración. En el ejemplo que he presentado, solo tiene que implementar el proyecto AdventureWorks.Database. Puede hacerlo desde Visual Studio (a través de la publicación) o mediante SqlPackage.exe. A continuación, se indica el comando para implementar la base de datos mediante SqlPackage.exe:

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

Cuando realiza una implementación incremental sobre una base de datos existente, existe la posibilidad de que el script más reciente necesite una migración. Eso significa que también tengo que implementar la base de datos de migración. Para hacerlo, implementaré primero el proyecto AdventureWorks.Database.Migration, seguido del proyecto AdventureWorks.Database. Asegúrese de que la opción "Volver a crear siempre la base de datos" esté desactivada en el área Opciones avanzadas de implementación del cuadro de diálogo Configuración de publicación avanzada, como se muestra en la Figura 6.

Cuadro de diálogo Configuración de publicación avanzada

Figura 6 Cuadro de diálogo Configuración de publicación avanzada

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

Tres problemas de migración comunes y sus correcciones

La migración de datos continua puede producir numerosas ventajas, pero también presenta sus desafíos. A continuación se indican algunos problemas comunes que puede encontrar al implementar esta solución, así como maneras de abordarlos.

Es posible que encuentre este error en el proyecto de migración, si su versión reciente de un script de migración ha quitado un objeto, pero se hace referencia al objeto en una versión anterior del script. La resolución consiste en escribir consultas como sp_executesql ‘<<su script de migración aquí>>.’ Por ejemplo:

EXEC sp_executesql 'ALTER TABLE Employee ADD NewCol INT'

Scripts de migración fuera de control y sobrecarga de versiones:

Se recomienda establecer siempre una versión de destino mínima para la migración. Ello limita el ámbito de los scripts de migración y contribuye a que no resulten demasiado difíciles de mantener.

Implementación con una base de datos de producción:

Si quiere implementar esta solución en una base de datos que ya está en producción, incluya la definición de internal.Database­Version y sus entradas de versión. Cambie el "Script 1" para ver si la tabla internal.DatabaseVersion existe y, en caso negativo, dirija la ejecución a la etiqueta de versión más reciente, que realizará la migración y creará la tabla. Por ejemplo:

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

Configuración de la compilación de TFS para implementar la migración continua

El objetivo es automatizar las migraciones del mismo modo que la integración continua, para que el servidor de compilación realice la migración de datos y la ponga a disposición de desarrolladores y evaluadores una vez desencadenada la compilación. El siguiente paso es configurar las tareas de lanzamiento de la compilación.

Para crear tareas para la compilación, primero debe tener en cuenta que debe crear una compilación de integración continua. De lo contrario, asegúrese de leer todo el tutorial publicado en el sitio de Microsoft Visual Studio en bit.ly/2xWqtUx.

Después de crear las tareas de compilación, deberá crear las tareas de implementación de la base de datos. En este ejemplo, debe agregar dos tareas de implementación: una para el proyecto AdventureWorks.Database.Migration y otra para el proyecto AdventureWorks.Database. La tarea de implementación tendrá un aspecto similar al de la Figura 7.

Tarea de implementación

Figura 7 Tarea de implementación

Rellene los detalles y configure el desencadenador en función de sus necesidades. Cuando la compilación esté en funcionamiento, deberá configurar una migración de datos continua para su aplicación.

Resumen

En este artículo, expliqué la importancia de la migración de datos continua en un proyecto que implica varias fases de lanzamiento, y como lograrla con Visual Studio y TFS. La migración de datos continua ayuda a reducir los esfuerzos de desarrollo y los errores de migración. En mi experiencia, he podido ahorrar hasta un 40 % de esfuerzo de migración en términos de desarrollo. También me ha permitido eliminar la fase de migración del proyecto.

La integración de los scripts de migración en TFS es tan importante como los propios scripts de migración. El proceso de migración de datos continua no sirve de nada si no se implementa como una parte de la compilación diaria. "Cometer errores pronto, cometer errores rápido" es el mantra que se debe recordar en el desarrollo de software y la migración de datos continua le expone precisamente a eso.


Jebarson Jebamony es asesor sénior de servicios Microsoft. Diseña y compila soluciones para Microsoft y sus asociados y clientes. Cuenta con más de 14 años de experiencia técnica y ha trabajado en numerosas tecnologías de Microsoft durante este tiempo.

Gracias a los siguientes expertos técnicos de Microsoft por revisar este artículo: Sagar Dheram y Shrenik Jhaveri


Discuta sobre este artículo en el foro de MSDN Magazine