От переводчика: Прекрасная статья на понимание механизма миграций в Entity Framework 6 и путей решения конфликтов миграций при работе в команде. Оригинал статьи: Code First Migrations in Team Environments.

Эта статья предполагает, что вы знакомы с Entity Framework и с основами работы с ним. Иначе сначала вам нужно прочитать Code First Migrations, прежде чем продолжить.

Налейте чашечку кофе, вам нужно прочитать всю статью


В командной работе проблемы в основном связаны со слиянием миграций, созданных двумя разработчиками в их локальных базах данных. В то время как шаги, для решения этих проблем довольно просты, они требуют, четкого представления о том, как работают миграции. Пожалуйста, не торопясь, прочитайте внимательно всю статью.

Некоторые общие принципы


Прежде чем мы углубимся в то, как управлять слиянием миграций создаваемых несколькими разработчиками, вот некоторые общие принципы.

Каждый член команды должен иметь локальную базу данных для разработки

Механизм миграций использует таблицу __MigrationsHistory, чтобы хранить список миграций, которые были применены к базе данных. Если у вас в команде несколько разработчиков генерируют миграции, то при попытке работы с одной и той же базой данных (и, следовательно, с одной таблицей __MigrationsHistory) механизм миграций может испытать затруднения.
Конечно, если у вас есть члены команды, которые не создают миграции, то проблем с работой с центральной базой данных для разработки не возникнет.

Избегайте автоматических миграций

Суть в том, что автоматические миграции изначально хорошо выглядят в командной работе, но в действительности они просто не работают. Если вы хотите знать причину, продолжайте читать статью, иначе вы можете перейти к следующей главе.

Автоматические миграции позволяют обновляться схеме базы данных в соответствии с текущей моделью без необходимости создания файлов с кодом миграции (code-based миграции).

Автоматические миграции будут хорошо работать в командной работе только если вы когда-нибудь использовали их и никогда не создавали никаких code-based миграций. Проблема в том, что автоматические миграции ограничены и не могут справиться с рядом операций — переименование свойства / колонки, переноса данных из одной таблицы в другую и т.д. Чтобы обработать такие сценарии, вы в конце концов будете создавать code-based миграции (и редактировать генерируемый код) что приводит к смешиванию изменений, которые обрабатываются с помощью автоматических миграций. Это делает практически невозможным слияние изменений двух разработчиков.

От переводчика: в оригинальной статье размещены 2 скринкаста, рекомендую ознакомиться

Принципы работы механизма миграций


Ключ к успешному использованию миграций в команде состоит в базовом понимании, как механизм миграции отслеживает и использует информацию о модели для обнаружения изменений.

Первая миграция

При добавлении первой миграции к вашему проекту, вы запускаете что-то вроде Add-Migration First в Package Manager Console. Внизу изображены шаги, которые выполняет эта команда.



На основе кода рассчитывается текущая модель (1). Затем с помощью model differ рассчитываются необходимые объекты базы данных (2) — поскольку это первая миграция, model differ для сравнения использует пустую модель. Необходимые изменения передаются в генератор кода для создания необходимого кода миграции (3), который затем добавляется в решение Visual Studio (4).

В дополнение к коду миграции, который хранится в главном файле, механизм миграции также создает дополнительные code-behind файлы. Это файлы метаданных, который используется механизмом миграций и вы не должны их изменять. Один из этих файлов — это файл ресурсов (.resx), который содержит снимок модели на момент создания миграции. В следующем разделе вы увидите, как он используется.

В этот момент можно выполнить Update-Database, чтобы применить изменения к базе данных, а затем начать реализовывать остальные части вашего приложения.

Последующие миграции

Внесем некоторые изменения в модель — в нашем примере мы добавим свойство Url в класс Blog. Затем необходимо выполнить команду Add-Migration AddUrl, чтобы создать миграцию для применения соответствующих изменений в базе данных. Внизу изображены шаги, которые выполняет эта команда.



Так же, как в прошлый раз, текущая модель рассчитывается из кода (1). Тем не менее, на этот раз есть существующие миграции, и предыдущая модель извлекается из последней миграции (2). Эти две модели сравниваются, чтобы найти необходимые изменения в базе данных (3), и затем процесс завершается, как и раньше.

Этот же процесс используется для всех следующих миграций, которые добавляются к проекту.

Зачем запариваться со снимками модели?


Вы можете быть удивлены, почему EF использует снимки модели для сравнения — почему бы просто не смотреть на базу данных.
Есть целый ряд причин, почему EF сохраняет состояния модели:
  • Это позволяет вашей базе данных отличаться от модели EF. Эти изменения могут быть сделаны непосредственно в базе данных, или вы можете изменить scaffolded код в ваших миграциях, чтобы внести изменения. Вот несколько примеров этого на практике:
    • Вы хотите добавить Inserted и Updated столбцы к одной или более таблиц, но вы не хотите, чтобы их включать в модель EF. Если бы механизм миграций смотрел на базу данных, он бы постоянно пытался удалить эти столбцы каждый раз, когда вы генерируете код миграции. Используя снимок модели, EF будет обнаруживать только нужные изменения в модели.
    • Вы хотите изменить тело хранимой процедуры, используемой для обновлений некой отладочной информации. Если бы механизм миграций смотрел на хранимую процедуру в БД, он бы постоянно пробовал сбросить её назад к определению. При использовании снимка модели, EF будет генерировать код для изменения хранимой процедуры, когда вы измените процедуру в модели EF
    • Эти же принципы применимы при добавлении дополнительных индексов, в том числе дополнительных таблиц в базе данных, маппинг EF к DB View и т.д.

  • Модель EF содержит больше, чем просто образ базы данных. Имея модель, механизм миграций позволяет просматривать информацию о свойствах и классах вашей модели и как они сопоставляются со столбцами и таблицами. Эта информация позволяет механизму миграции быть умнее при автоматической генерации кода. Например, если вы переименовываете столбец, маппинг миграции обнаружит переименование, видя, что это то же самое свойство. Это было бы невозможно сделать, если бы мы смотрели только на базу данных

Что вызывает вопросы при командной работе


Процесс, рассмотренный в предыдущем разделе, прекрасно работает, если вы единственный разработчик, который работает над приложением. Он также хорошо работает в команде, если вы единственный человек, который вносит изменения в модель. В этом случае вы можете вносить изменения в модель, генерировать миграции и отправлять их в систему контроля версий. Другие разработчики могут получать такие изменения и запускать Update-Database, чтобы обновлять схему.

Проблемы начинают возникать, когда у вас несколько разработчиков, которые вносят изменения в модель EF и отправляют их в систему контроля версий. Чего EF не хватает, так это первоклассного способа объединять локальные миграции с миграциями, других разработчиков отправленных в систему контроля версий после последней синхронизации.

Пример слияния конфликта


Сначала давайте рассмотрим конкретный пример слияния такого конфликта. Мы будем продолжать на примере, который рассмотрели ранее. В качестве отправной точки давайте предположим, что изменения от предыдущего раздела были проверены настоящим разработчиком. Мы будем отслеживать двух разработчиков, которые делают изменения в коде.

Мы будем отслеживать модель EF и миграции через ряд изменений. Оба разработчика синхронизируются через репозиторий в системе контроля версий, как показано на следующем рисунке.



Разработчик #1 и разработчик #2 делают некоторые изменения в модели EF в локальной кодовой базе. Разработчик #1 добавляет свойство Rating в класс Blog, создает миграцию AddRating для применения изменений в базу данных. Разработчик #2 добавляет свойство Readers в класс Blog, создает миграцию AddReaders. Оба разработчика запускают Update-Database, чтобы применить изменения к их локальным базам данных, а затем продолжают разработку приложения.

Примечание: Миграции начинаются с метки времени, так что наш рисунок показывает, что миграция AddReaders от разработчика #2 приходит после миграции AddRating от разработчика #1. С точки зрения работы в команде, нам без разницы в каком порядке создавались эти изменения, процесс их объединения мы рассмотрим в следующем разделе.



Сегодня повезло разработчику #1, так как он первый отправит свои изменения в систему контроля версий. Так как никто другой не отправлял изменения в репозиторий, он может просто отправить свои изменения, не выполняя никакого слияния.



Теперь пришло время для разработчика #2 отправить изменения. Он не такой везучий. Т.к. кто-то опубликовал изменения после последней синхронизации, разработчик должен забрать их и провести слияние. Система контроля версий, скорее всего, сможет автоматически объединить изменения на уровне кода, так как они очень просты. Состояние локальной кодовой базы разработчика #2 после синхронизации изображено на следующем рисунке.



На этом этапе разработчик #2 может запустить Update-Database, который позволяет обнаружить новую AddRating миграцию (который не был применен к базе данных разработчика #2), и применить его. Теперь столбец Rating добавлен к таблице Blogs, и база данных синхронизирована с моделью.

Есть несколько проблем, например:

  1. Хотя операция Update-Database будет применять миграции AddRating, она также будет показывать предупреждение:
    Unable to update database to match the current model because there are pending changes and automatic migration is disabled…
    Проблема в том, что снимок модели хранится в последней миграции (AddReader), которая пропускает свойство Rating в классе Blog (так как он не был частью модели, когда миграция была создана).

    Code First обнаруживает, что модель в прошлой миграции не соответствует текущей модели и отображает предупреждение.
  2. Запуск приложения приведет к InvalidOperationException «The model backing the 'BloggingContext' context has changed since the database was created. Consider using Code First Migrations to update the database…»

    Опять же, проблема заключается в том, что снимок модели хранится в последней миграции, не соответствующей текущей модели
  3. Наконец, можно было бы ожидать запуск Add-Migration теперь будет генерировать пустую миграцию (так как нет никаких изменений, которые необходимо применить к базе данных). Но так как миграции сравнивают текущую модель в одной из последних миграций (в которой отсутствует свойство Rating) это будет генерировать другой AddColumn вызов, чтобы добавить столбец Rating.

    Конечно, эта миграция потерпит неудачу при Update-Database, потому что столбец Rating уже существует.

Решение конфликтов слияния


Хорошей новостью является то, что не очень трудно сливать миграции вручную, если у вы понимаете, как работает миграция. Так что если вы пропустили начало этой статьи… извините, нужно вернуться назад и прочитать первую часть статьи сначала!

Есть два варианта, самый простой заключается в создании пустой миграции, который имеет правильный снимок модели. Второй вариант заключается в обновлении снимка в последней миграции, чтобы иметь правильный снимок модели. Это немного сложнее и этот вариант не может быть использован в каждом случае. Его преимущество в том, что он не предполагает добавление дополнительной миграции.

Вариант 1: Добавление пустой «merge» миграции


В этом варианте мы генерируем пустую миграцию исключительно для того, чтобы последняя миграция имела правильный снимок модели, хранящийся в нем.

Эта возможность может быть использована независимо от того, кто создал последнюю миграцию. В примере мы наблюдали за разработчиком #2, который генерировал последнюю миграцию. Но эти же шаги могут быть использованы, если бы разработчик #1 сгенерировал последнюю миграцию. Шаги также применяться, если есть несколько миграций.

Следующий алгоритм может быть использован с момента, когда появляются изменения, которые должны быть синхронизированы с системой контроля версий.

  1. Убедитесь, что все изменения модели в вашей локальной кодовой базе были сохранены в миграции. Этот шаг гарантирует, что вы не пропустите никакие важные изменения, когда придет время создания чистой миграции
  2. Синхронизируйте код с системой контроля версий
  3. Выполните Update-Database для применения любых новых миграций, сделанных другими разработчиками.
    Примечание: если вы не получите каких-либо предупреждений во время выполнения Update-Database, то не было никаких новых миграций от других разработчиков и нет необходимости выполнять дополнительные слияния.
  4. Выполните Add-Migration <pick_a_name> -ignorechanges (например, add-migration merge -ignorechanges). Эта команда создает миграцию со всеми метаданными (включая снимок текущей модели), но будет игнорировать любые изменения, которые он обнаружит при сравнении текущей модели со снимком последний миграции (то есть вы получите пустые Up и Down методы).
  5. Продолжайте разработку, или отправляйте изменения в систему управления версий (после запуска модульных тестов конечно).

Это состояние модели разработчика #2 после применения этого подхода.



Вариант 2: Обновление снимка модели последней миграции


Этот вариант очень похож на вариант 1, но удаляет лишнюю пустую миграцию.

Этот подход возможен, если последняя миграция существует только локально и до сих пор не отправлен в систему контроля версий (т.е. последняя миграция была создана пользователем выполняющим слияние). Редактирование метаданных миграций, которые применены у других разработчиков, или даже хуже — применены к боевой базе данных, может привести к неожиданным побочным эффектам. В процессе мы будем выполнять откат последней миграции в нашей локальной базе данных и повторно применять с обновленными метаданными.

Пока последняя миграция находится локально, нет никаких ограничений на количество или порядок миграций.
Те же шаги применимы, при нескольких миграциях от нескольких различных разработчиков.

Следующий алгоритм может быть использован, когда появляются изменения, которые должны быть синхронизированы с системой контроля версий.

  1. Убедитесь, что все изменения модели в вашей локальной кодовой базе были сохранены в миграции.
    Этот шаг гарантирует, что вы не пропустите никакие важные изменения, когда придет время создания чистой миграции.
  2. Синхронизируйте код с системой контроля версий.
  3. Выполните Update-Database для применения любых новых миграций, сделанных другими разработчиками.

    Примечание: если вы не получите каких-либо предупреждений во время выполнения Update-Database, то не было никаких новых миграций от других разработчиков и нет необходимости выполнять дополнительные слияния.
  4. Запустите Update-Database -TargetMigration <second_last_migration> (в примере это было бы Update-Database -TargetMigration AddRating).

    Это действие откатывает базу данных назад на состояние предпоследней миграции — фактически,
    где не была применена последняя миграция на базу данных.

    Примечание: Этот шаг необходим, чтобы сделать безопасным редактирование метаданных миграции, поскольку метаданные также хранятся в таблице __MigrationsHistory в базы данных. Вот почему вы должны использовать эту функцию, только если последняя миграция есть только локально. Если на других базах данных необходимо применить последнюю миграцию, вам необходимо также откатить её назад и переприменить последнюю миграцию для обновления метаданных.
  5. Выполните Add-Migration <full_name_including_timestamp_of_last_migration> (в примере это было бы что-то вроде надстроек миграции Add-Migration 201311062215252_AddReaders).

    Примечание: Необходимо указать метку так, чтобы механизм миграций понял, что вы хотите изменить существующую миграцию, а не создать новую. Это позволит обновить метаданные для последней миграции, чтобы соответствовать текущей модели. Вы получите следующее предупреждение при завершении команды, но это именно то, что вы хотите. “Only the Designer Code for migration '201311062215252_AddReaders' was re-scaffolded. To re-scaffold the entire migration, use the -Force parameter.”
  6. Запустите Update-Database, чтобы повторно применить последнюю миграцию с обновленными метаданными.
  7. Продолжайте разработку, или отправляйте изменения в систему управления версий (после запуска модульных тестов конечно).

Вот это состояние локальной кодовой базы разработчика #2 после применения этого подхода.



Итого


Есть некоторые проблемы при использовании миграций Code First в команде. Тем не менее, общее представление о том работают миграции, и некоторые простые подходы к решению конфликтов слияния позволяют легко преодолеть эти проблемы.

Фундаментальной проблемой являются ошибочные метаданные, которые хранятся в последней миграции. Это позволяет Code First неправильно определять, что текущая модель и схема базы данных не совпадают и сгенерировать неправильный код для следующей миграции. Эта ситуация может быть исправлена путем генерации пустой миграции с правильной моделью, или обновления метаданных в последней миграции.

Комментарии (3)


  1. AxisPod
    25.02.2016 16:44

    Очень важная статья, читал еще помнится в оригинале. Она спасла нас в самые тяжелые моменты жизни нашей команды :-)


    1. nkochnev
      25.02.2016 19:42

      Был очень удивлен, когда понял, что нет статьи подобной тематики на русском языке =)


  1. kemsky
    26.02.2016 14:08

    Подскажите, существуют ли для .net сторонние библиотеки для поддержки миграции БД, такие как http://www.liquibase.org/.