Code First Migrations в средах командной работы

Примечание.

В этой статье предполагается, что вы умеете использовать Code First Migrations в основных сценариях. Если вы этого не сделали, прежде чем продолжить, вам потребуется прочитать code First Migrations .

Возьмите кофе, вам нужно прочитать эту статью полностью.

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

Некоторые общие рекомендации

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

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

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

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

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

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

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

Общие сведения о том, как работает миграция

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

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

Когда вы добавляете первую миграцию в проект, вы выполняете что-то вроде команды Add-Migration First в консоли диспетчера пакетов. Ниже приведены шаги высокого уровня, которые выполняет эта команда.

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

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

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

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

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

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

Вторая миграция

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

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

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

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

Существует ряд причин, по которым EF сохраняет моментальный снимок модели:

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

Что приводит к проблемам в средах группы

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

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

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

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

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

Начальная точка

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

Примечание.

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

Локальные изменения

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

Отправка изменений

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

Выгрузка из системы управления версиями

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

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

  1. Несмотря на то что Update-Database будет применять миграцию AddRating, она также вызовет предупреждение: не удается обновить базу данных в соответствии с текущей моделью, так как существуют ожидающие изменения и автоматическая миграция отключена... Проблема заключается в том, что моментальный снимок модели, хранящийся в последней миграции (AddReader), отсутствует свойство Rating в Blog (так как оно не было частью модели при создании миграции). Код First обнаруживает, что модель, используемая в последней миграции, не соответствует текущей модели, и выдаёт предупреждение.
  2. Запуск приложения приведет к ошибке InvalidOperationException с сообщением о том, что модель, поддерживающая контекст BloggingContext, изменилась с момента создания базы данных. Рассмотрите возможность использования code First Migrations для обновления базы данных..." Опять же, проблема заключается в том, что моментальный снимок модели, хранящийся в последней миграции, не соответствует текущей модели.
  3. Наконец, мы ожидаем, что запуск Add-Migration сейчас создаст пустую миграцию (поскольку нет изменений для применения к базе данных). Но поскольку миграции сравнивают текущую модель с моделью из последней миграции (в которой отсутствует свойство Rating), она фактически сгенерирует другой вызов AddColumn, чтобы добавить столбец Rating. Конечно, эта миграция завершится ошибкой во время обновления базы данных , так как столбец "Рейтинг " уже существует.

Разрешение конфликта слияния

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

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

Вариант 1. Создание пустой миграции «слияние»

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

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

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

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

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

Миграция слиянием

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

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

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

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

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

  1. Убедитесь, что все ожидающие изменения модели в локальной базе кода были записаны в миграцию. Этот шаг гарантирует, что вы не пропустите никаких законных изменений, когда приходит время создать пустую миграцию.
  2. Синхронизация с системой управления версиями.
  3. Запустите update-Database , чтобы применить любые новые миграции, которые другие разработчики выполнили. Примечание.Если от команды Update-Database не поступает никаких предупреждений, значит, новых миграций от других разработчиков нет, и слияние выполнять не требуется.
  4. Запустите Update-Database –TargetMigration <second_last_migration> (в примере, который мы следовали, это будет Update-Database –TargetMigration AddRating). При этом база данных откатится к состоянию предпоследней миграции, фактически отменяя последнюю миграцию из базы данных. Примечание.Этот шаг необходим, чтобы сделать его безопасным для изменения метаданных миграции, так как метаданные также хранятся в __MigrationsHistoryTable базы данных. Поэтому этот параметр следует использовать только в том случае, если последняя миграция находится только в локальной базе кода. Если к другим базам данных применена последняя миграция, их также придется откатить и повторно применить последнюю миграцию для обновления метаданных. 
  5. Запустите Add-Migration <full_name_including_timestamp_of_last_migration> (в примере, который мы следовали, это будет что-то вроде Add-Migration 201311062215252_AddReaders). Примечание.Необходимо включить метку времени, чтобы миграции знали, что вы хотите изменить существующую миграцию, а не создать новую. Это приведет к обновлению метаданных для последней миграции в соответствии с текущей моделью. Вы получите следующее предупреждение, когда команда завершится, но это как раз то, что вам нужно. "Только код конструктора для миграции "201311062215252_AddReaders" был повторно создан. Чтобы повторно создать шаблон всей миграции, используйте параметр -Force".
  6. Запустите update-Database , чтобы повторно применить последнюю миграцию с обновленными метаданными.
  7. Продолжайте разработку или отправьте в системы управления версиями (после выполнения модульных тестов, конечно).

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

Обновленные метаданные

Итоги

При использовании code First Migrations в среде группы возникают некоторые проблемы. Однако базовое понимание работы миграций и некоторые простые подходы к решению конфликтов слияния позволяют легко преодолеть эти проблемы.

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