Code First Migrations in Team Environments (Migraciones de Code First en entornos de equipo)

Nota:

En este artículo se da por supuesto que sabe cómo usar migraciones de Code First en escenarios básicos. De lo contrario, tendrá que leer Migraciones de Code First antes de continuar.

Prepare un café, ya que debe leer todo este artículo completo

Los problemas de los entornos de equipo suelen estar relacionados con la combinación de migraciones cuando dos desarrolladores han generado migraciones en su base de código local. Aunque los pasos para resolverlos son bastante sencillos, se necesita una comprensión sólida de cómo funcionan las migraciones. No vaya directamente al final; tómese el tiempo necesario para leer todo el artículo y asegurarse de que tiene éxito.

Instrucciones generales

Antes de profundizar en cómo administrar las migraciones combinadas generadas por varios desarrolladores, estas son algunas instrucciones generales para que el proceso sea correcto.

Cada miembro del equipo debe tener una base de datos de desarrollo local

Las migraciones usan la tabla __MigrationsHistory para almacenar las migraciones que se han aplicado a la base de datos. Si hay varios desarrolladores que generan migraciones diferentes al intentar dirigirse a la misma base de datos de destino (y, por tanto, comparten una tabla __MigrationsHistory), las migraciones serán muy confusas.

Por supuesto, si hay miembros del equipo que no generan migraciones, no hay ningún problema al compartir una base de datos de desarrollo central.

Evitar migraciones automáticas

La conclusión es que las migraciones automáticas son inicialmente buenas en entornos de equipo, pero en realidad no funcionan correctamente. Si quiere saber por qué, siga leyendo; de lo contrario, puede ir directamente a la sección siguiente.

Las migraciones automáticas permiten actualizar el esquema de la base de datos para que coincida con el modelo actual sin necesidad de generar archivos de código (migraciones basadas en código). Las migraciones automáticas funcionarían muy bien en un entorno de equipo si solo las ha usado y nunca ha generado migraciones basadas en código. El problema es que las migraciones automáticas están limitadas y no controlan una serie de operaciones: cambios de nombre de propiedad o columna, movimiento de datos a otra tabla, etc. Para controlar estos escenarios, termina generando migraciones basadas en código (y edita el código con scaffolding) que se mezclan entre los cambios que se controlan mediante migraciones automáticas. Esto hace que sea casi imposible combinar los cambios cuando dos desarrolladores confirman las migraciones.

Descripción del funcionamiento de las migraciones

La clave para usar correctamente migraciones en un entorno de equipo consiste en tener conocimientos básicos de cómo las migraciones realizan el seguimiento de la información sobre el modelo para detectar cambios en el modelo y cómo la usan.

La primera migración

Al agregar la primera migración al proyecto, ejecute algo parecido a Agregar la primera migración en la consola del Administrador de paquetes. Los pasos generales que realiza este comando se describen a continuación.

First Migration

Se calcula el modelo actual a partir del código (1). Después, el modelo calcula los objetos de base de datos necesarios (2); como esta es la primera migración que el modelo difiere simplemente usa un modelo vacío para la comparación. Los cambios necesarios se pasan al generador de código para compilar el código de migración necesario (3) que luego se agrega a la solución de Visual Studio (4).

Además del código de migración real almacenado en el archivo de código principal, las migraciones también generan varios archivos de código subyacente adicionales. Estos archivos son metadatos que usan las migraciones y no los debe editar. Uno de ellos es un archivo de recursos (.resx) que contiene una instantánea del modelo en el momento en que se ha generado la migración. En el paso siguiente verá cómo se usa.

En este momento probablemente ejecutaría Update-Database para aplicar los cambios en la base de datos y, después, continuaría con la implementación de otras áreas de la aplicación.

Migraciones posteriores

Más adelante vuelve y realiza algunos cambios en el modelo; en el ejemplo, se agregará una propiedad Url a Blog. Después, se emite un comando como Add-Migration AddUrl para aplicar scaffolding a una migración a fin de aplicar los cambios de base de datos correspondientes. Los pasos generales que realiza este comando se describen a continuación.

Second Migration

Como en el caso anterior, el modelo actual se calcula a partir del código (1). Pero esta vez hay migraciones existentes, por lo que el modelo anterior se recupera de la migración más reciente (2). Estos dos modelos se diferencian para encontrar los cambios de base de datos necesarios (3) y, después, el proceso se completa como antes.

Este mismo proceso se usa para cualquier migración adicional que agregue al proyecto.

¿Por qué molestarse con la instantánea del modelo?

Es posible que se pregunte por qué EF se molesta con la instantánea del modelo y por qué no examina simplemente la base de datos. Si es así, siga leyendo. Si no le interesa, puede omitir esta sección.

Hay varias razones por las que EF mantiene la instantánea del modelo:

  • Le permite que la base de datos se desfase del modelo de EF. Estos cambios se pueden realizar directamente en la base de datos, o bien puede cambiar el código con scaffolding en las migraciones para realizar los cambios. Estos son un par de ejemplos prácticos:
    • Quiere agregar una columna Inserted y Updated a una o varias de las tablas, pero no quiere incluir estas columnas en el modelo de EF. Si las migraciones examinan la base de datos, intentarían quitar continuamente estas columnas cada vez que se aplica scaffolding a una migración. Con la instantánea del modelo, EF solo detectará cambios legítimos en el modelo.
    • Quiere cambiar el cuerpo de un procedimiento almacenado que se usa en las actualizaciones para incluir algún tipo de registro. Si las migraciones examinan este procedimiento almacenado desde la base de datos, intentarán continuamente restablecerlo a la definición que EF espera. Con la instantánea del modelo, EF solo aplicará scaffolding al código para modificar el procedimiento almacenado cuando cambie su forma en el modelo de EF.
    • Estos mismos principios se aplican a la adición de índices adicionales, incluidas las tablas adicionales de la base de datos, la asignación de EF a una vista de base de datos que se encuentra sobre una tabla, etc.
  • El modelo de EF contiene algo más que la forma de la base de datos. Tener todo el modelo permite a las migraciones ver información sobre las propiedades y clases del modelo, y cómo se asignan a las columnas y tablas. Esta información permite que las migraciones sean más inteligentes en el código que aplica scaffolding. Por ejemplo, si cambia el nombre de la columna que una propiedad asigna a migraciones, puede detectar el cambio de nombre al ver que es la misma propiedad, algo que no se puede hacer si solo tiene el esquema de la base de datos. 

¿Qué causa problemas en entornos de equipo?

El flujo de trabajo descrito en la sección anterior funciona bien cuando un único desarrollador trabaja en una aplicación. También funciona bien en un entorno de equipo si es la única persona que realiza cambios en el modelo. En este escenario, puede realizar cambios en el modelo, generar migraciones y enviarlas al control de código fuente. Otros desarrolladores pueden sincronizar los cambios y ejecutar Update-Database para que se apliquen los cambios de esquema.

Los problemas comienzan a surgir cuando hay varios desarrolladores que realizan cambios en el modelo de EF y se envían al control de código fuente al mismo tiempo. EF carece de una manera de combinar las migraciones locales con las que otro desarrollador ha enviado al control de código fuente desde la última sincronización.

Ejemplo de conflicto de combinación

En primer lugar, se examinará un ejemplo concreto de este conflicto de combinación. Se continuará con el ejemplo anterior. Como punto de partida, imagine que el desarrollador original ha insertado en el repositorio los cambios de la sección anterior. Se realizará el seguimiento de dos desarrolladores a medida que realizan cambios en la base de código.

Se realizará el seguimiento del modelo de EF y las migraciones a través de una serie de cambios. Como punto de partida, ambos desarrolladores se han sincronizado con el repositorio de control de código fuente, como se muestra en el gráfico siguiente.

Starting Point

Ahora el desarrollador 1 y el desarrollador 2 realizan algunos cambios en el modelo de EF en su base de código local. El desarrollador 1 agrega una propiedad Rating a Blog y genera una migración de AddRating para aplicar los cambios en la base de datos. El desarrollador 2 agrega una propiedad Reader a Blog y genera la migración de AddReaders correspondiente. Los dos desarrolladores ejecutan Update-Database, para aplicar los cambios a sus bases de datos locales y, después, continúan con el desarrollo de la aplicación.

Nota:

Las migraciones tienen como prefijo una marca de tiempo, por lo que en el gráfico se representa que la migración de AddReaders del desarrollador 2 es posterior a la migración AddRating del desarrollador 1. Que el desarrollador 1 o el 2 haya generado primero la migración es indiferente para los problemas de trabajo en un equipo o para el proceso de combinación que se verá en la sección siguiente.

Local Changes

Es un día de suerte para el desarrollador 1, ya que es el primero en enviar los cambios. Como nadie más ha realizado la inserción desde que ha sincronizado su repositorio, puede enviar simplemente los cambios sin realizar ninguna combinación.

Submit Changes

Ahora es el momento de que el desarrollador 2 envíe los cambios. No tendrá tanta suerte. Como otra persona ha enviado cambios desde que ha realizado la sincronización de datos, tendrá que extraerlos y combinarlos. Es probable que el sistema de control de código fuente pueda combinar automáticamente los cambios en el nivel de código, ya que son muy sencillos. En el gráfico siguiente se muestra el estado del repositorio local del desarrollador 2 después de la sincronización. 

Pull From Source Control

En esta fase, el desarrollador 2 puede ejecutar Update-Database que detectará la nueva migración de AddRating (que no se ha aplicado a la base de datos del desarrollador 2) y aplicarla. Ahora la columna Rating se ha agregado a la tabla Blogs y la base de datos está sincronizada con el modelo.

Pero hay un par de problemas:

  1. Aunque Update-Database aplicará la migración AddRating también generará una advertencia: No se puede actualizar la base de datos para que coincida con el modelo actual porque hay cambios pendientes y la migración automática está deshabilitada… El problema es que en la instantánea del modelo almacenada en la última migración (AddReader) falta la propiedad Rating en Blog (ya que no formaba parte del modelo cuando se ha generado la migración). Code First detecta que el modelo de la última migración no coincide con el modelo actual y genera la advertencia.
  2. Al ejecutar la aplicación aparecería una excepción InvalidOperationException con el texto "El modelo que respalda el contexto "BloggingContext" ha cambiado desde que se ha creado la base de datos. Considere la posibilidad de usar migraciones de Code First para actualizar la base de datos…" De nuevo, el problema es que la instantánea del modelo almacenada en la última migración no coincide con el modelo actual.
  3. Por último, se esperaría que al ejecutar Add-Migration ahora se genere una migración vacía (ya que no hay ningún cambio para aplicar a la base de datos). Pero como las migraciones comparan el modelo actual con el de la última migración (donde falta la propiedad Rating), en realidad se aplicará scaffolding a otra llamada a AddColumn para agregar la columna Rating. Por supuesto, esta migración produciría un error durante Update-Database porque la columna Rating ya existe.

Resolución del conflicto de combinación

La buena noticia es que no es demasiado difícil controlar la combinación manualmente, siempre que comprenda cómo funcionan las migraciones. Por tanto, si ha pasado directamente a esta sección… desafortunadamente tendrá que retroceder y leer primero el resto del artículo.

Hay dos opciones, lo más fácil es generar una migración en blanco que tenga el modelo actual correcto como una instantánea. La segunda opción consiste en actualizar la instantánea en la última migración para que tenga la instantánea del modelo correcta. La segunda opción es un poco más difícil y no se puede usar en todos los escenarios, pero también es más limpia, porque no implica agregar una migración adicional.

Opción 1: Adición de una migración de "combinación" en blanco

En esta opción, se genera una migración en blanco únicamente para asegurarse de que la migración más reciente tiene almacenada la instantánea correcta del modelo.

Esta opción se puede usar independientemente de quién haya generado la última migración. En el ejemplo, se ha visto que el desarrollador 2 se encarga de la combinación y ha generado la última migración. Pero se pueden usar estos mismos pasos si es el desarrollador 1 el que ha generado la última migración. Los pasos también se aplican si hay varias migraciones implicadas; simplemente se han examinado dos para simplificarlo.

El siguiente proceso se puede usar para este enfoque, comenzando desde el momento en que se da cuenta de que tiene cambios que se deben sincronizar desde el control de código fuente.

  1. Asegúrese de que los cambios del modelo pendientes en la base de código local se han escrito en una migración. Este paso garantiza que no se pierdan los cambios legítimos cuando llegue el momento de generar la migración en blanco.
  2. Sincronización con el control de código fuente
  3. Ejecute Update-Database para aplicar las nuevas migraciones que otros desarrolladores hayan insertado en el repositorio. Nota: Si no recibe ninguna advertencia del comando Update-Database, significa que no había nuevas migraciones de otros desarrolladores y no es necesario realizar ninguna combinación adicional.
  4. Ejecute Add-Migration <pick_a_name> –IgnoreChanges (por ejemplo, Add-Migration Merge -IgnoreChanges). Esto genera una migración con todos los metadatos (incluida una instantánea del modelo actual), pero omitirá los cambios que detecte al comparar el modelo actual con la instantánea en las últimas migraciones (lo que significa que se obtiene un método Up y Down en blanco).
  5. Ejecute Update-Database para volver a aplicar la migración más reciente con los metadatos actualizados.
  6. Continúe con el desarrollo o realice el envío al control de código fuente (después de ejecutar las pruebas unitarias por supuesto).

Este es el estado del código local del desarrollador 2 después de usar este enfoque.

Merge Migration

Opción 2: Actualización de la instantánea del modelo en la última migración

Esta opción es muy similar a la opción 1, pero quita la migración en blanco adicional porque, para ser claros, nadie quiere archivos de código adicionales en su solución.

Este enfoque solo es factible si la migración más reciente solo existe en la base de código local y aún no se ha enviado al control de código fuente (por ejemplo, si el usuario que realiza la combinación es el que ha generado la última migración). La edición de los metadatos de las migraciones que otros desarrolladores pueden haber aplicado a su base de datos de desarrollo, o peor todavía, a una base de datos de producción puede dar lugar a efectos secundarios inesperados. Durante el proceso, se revertirá la última migración de la base de datos local y se volverá a aplicar con metadatos actualizados.

Aunque la última migración solo debe estar en la base de código local, no hay restricciones en el número o el orden de las migraciones siguientes. Puede haber varias migraciones de varios desarrolladores diferentes y se aplicarán los mismos pasos; simplemente se han examinado dos para simplificarlo.

El siguiente proceso se puede usar para este enfoque, comenzando desde el momento en que se da cuenta de que tiene cambios que se deben sincronizar desde el control de código fuente.

  1. Asegúrese de que los cambios del modelo pendientes en la base de código local se han escrito en una migración. Este paso garantiza que no se pierdan los cambios legítimos cuando llegue el momento de generar la migración en blanco.
  2. Realice la sincronización con el control de código fuente.
  3. Ejecute Update-Database para aplicar las nuevas migraciones que otros desarrolladores hayan insertado en el repositorio. Nota: Si no recibe ninguna advertencia del comando Update-Database, significa que no había nuevas migraciones de otros desarrolladores y no es necesario realizar ninguna combinación adicional.
  4. Ejecute Update-Database –TargetMigration <second_last_migration> (en el ejemplo anterior esto sería Update-Database –TargetMigration AddRating). Esto revierte la base de datos al estado de la penúltima migración, lo que deshace eficazmente la aplicación de la última migración de la base de datos. Nota:Este paso es necesario para que sea seguro editar los metadatos de la migración, ya que también se almacenan en __MigrationsHistoryTable en la base de datos. Por este motivo únicamente debe usar esta opción si la última migración solo está en la base de código local. Si otras bases de datos tuvieran aplicada la última migración, también tendría que revertirlas y volver a aplicar la última migración para actualizar los metadatos. 
  5. Ejecute Add-Migration <full_name_including_timestamp_of_last_migration> (en el ejemplo sería algo parecido a Add-Migration 201311062215252_AddReaders). Nota:Debe incluir la marca de tiempo para que las migraciones sepan que quiere editar la migración existente en lugar de aplicar scaffolding a una nueva. Esto actualizará los metadatos de la última migración para que coincida con el modelo actual. Recibirá la siguiente advertencia cuando se complete el comando, pero eso es exactamente lo que quiere. "Solo se ha vuelto a aplicar scaffolding al código del diseñador para la migración "201311062215252_AddReaders". Para volver a aplicar scaffolding a toda la migración, use el parámetro -Force."
  6. Ejecute Update-Database para volver a aplicar la migración más reciente con los metadatos actualizados.
  7. Continúe con el desarrollo o realice el envío al control de código fuente (después de ejecutar las pruebas unitarias por supuesto).

Este es el estado del código local del desarrollador 2 después de usar este enfoque.

Updated Metadata

Resumen

Hay algunos desafíos al usar migraciones de Code First en un entorno de equipo. Pero una comprensión básica de cómo funcionan las migraciones y algunos enfoques sencillos para resolver conflictos de combinación facilitan la solución de estos desafíos.

El problema fundamental es el de los metadatos incorrectos almacenados en la migración más reciente. Esto hace que Code First detecte incorrectamente que el modelo actual y el esquema de base de datos no coinciden y se aplique scaffolding a código incorrecto en la siguiente migración. Esta situación se puede superar mediante la generación de una migración en blanco con el modelo correcto, o bien con la actualización de los metadatos en la migración más reciente.