воскресенье, 15 января 2012 г.

Code First Migrations + Entity Framework 4.3 Beta 1

MonarchButterflyНа днях, а точнее 12 января увидел свет Entity Framework 4.3 Beta 1. Основная тема этого релиза – EF Code First Migrations. Помимо этого, есть несколько изменений, которые тоже имеет смысл упомянуть.

На фото миграция бабочки монарх (CC BY-NC-ND 2.0, оригинал здесь).

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

  • Что такое миграции и для чего они нужны?
  • Что интересного в этом релизе и в EF Code First Migrations вообще?

Думаю, вряд ли кто-то использует миграции в повседневной коммерческой разработке. Поэтому я не буду явно выделять список нововведений, а буду просто рассказывать про миграции, отдельно выделяя новое в EF 4.3 Beta 1.

Кстати, наверное вам будет интересно, что релиз EF 4.3 планируется на первый квартал этого года. Жаль только что релиз EF 5.0 с поддержкой enum и оптимизированной скоростью ждёт наc нескоро – с выходом .NET 4.5 ожидается только EF 5.0 Beta 1.

Краткое описание миграций

Миграции в EF предназначены для решения проблемы изменения структуры базы данных в процессе эволюции приложения. Один из вариантов решения этой проблемы я озвучивал ранее в статье “Основы SQL - DDL и рефакторинг БД”. Надеюсь, что когда будет готов EF Code First Migrations, он будет значительно лучше, чем подход со скриптами.

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

Вряд ли вам хочется после установки новой версии клиенту (или, что хуже – клиентам) узнать, что система автоматической миграции удалила некоторые данные и клиент подумывает о том, за счёт кого компенсировать потери своих денег…

Далее я приведу типичный пример работы с миграциями, однако сначала давайте всё установим.

Как установить?

Во-первых хочу отметить, что это Beta, со всеми вытекающими рисками. Во-вторых, Code First Migrations теперь включены в EF и их не нужно устанавливать отдельно.

Наверняка вы знаете, что установку NuGet-пакетов проще всего делать в Package Manager Console (далее PM), поэтому буду рассматривать только этот способ. Последовательность действий по установке такая:

  • Если был установлен пакет “Code First Migrations Beta 1”:
    PM> Uninstall-Package EntityFramework.Migrations
  • Устанавливаем EF 4.3 Beta 1 с параметром IncludePrerelease:
    PM> Install-Package EntityFramework –IncludePrerelease
  • Перезапускаем студию. В этом случае команды для PM я не знаю :)

Для ускорения написания примера я использую Model Scaffolding. Поэтому добавлю еще один необязательный, в общем случае, шаг:

PM> Install-Package ModelScaffolding

Примеры работы с миграциями

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

Вы ещё здесь? :) Тогда давайте сначала рассмотрим простой пример – добавляем модель, потом добавляем к ней новое свойство, а потом удалим его. При этом будем мигрировать данные с помощью новых встроенных механизмов.

Настройка соединения с базой данных

Я буду использовать SQL Server Express, как вариант, предлагаемый по умолчанию. Для быстрого подключения нашего DbContext в web.config скопируем существующую строку соединения (по умолчанию используется ApplicationServices) и добавим её с названием, совпадающим с названием контекста:

<add name="MvcEfDemoContext" providerName="System.Data.SqlClient"
connectionString="data source=.\SQLEXPRESS;Database=aspnetdb.mdf;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|aspnetdb.mdf;User Instance=true" />
<add name="ApplicationServices" providerName="System.Data.SqlClient"
connectionString="data source=.\SQLEXPRESS;Database=aspnetdb.mdf;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|aspnetdb.mdf;User Instance=true" />

Важное замечание: без фразы “Database=aspnetdb.mdf” приложение и миграции будут работать некорректно (по умолчанию эта фраза не присутствует в строке соединения).

Создание первого варианта модели

Теперь, для создания простейшей модели введём следующую команду:

PM> Scaffold Model Test -Controller

Если вы читали мою статью про ModelScaffolding вы уже знаете, что эта команда создаст следующую модель (и всю обвязку для её редактирования в ASP.NET MVC приложении – контроллер и представления):

public class Test
{
public int Id {get; set;}
[StringLength(50)]
[Required]
public string Name {get; set;}
}

Давайте запустим наше приложение, добавив к URL “Tests” (можно это сделать сразу в настройке проекта, чтобы сэкономить время в дальнейшем) и введём немного данных.

NewTestRecords

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

Забегая вперёд скажу, что несмотря на предупреждение из блога команды ADO.NET ниже, с багами мне так и не довелось встретиться, видимо было слишком позитивное настроение :)

NOTE: There are a number of bugs in the scripting functionality in EF 4.3 Beta1 that prevent you generating a script starting from a migration other than an empty database. These bugs will be fixed in the final RTM.

Изменение модели и первая миграция

Для того, чтобы включить миграцию, теперь (начиная с версии EF 4.3) можно просто запустить следующую команду:

PM> Enable-Migrations

Эта команда добавит к проекту папку “Migrations” и файл “Configuration.cs”. Если вы согласны с поведением по умолчанию, то есть с отключением автоматических миграций, ничего больше делать не надо. В случае, когда DbContext-ов будет много, потребуется немного дописать этот файл вручную.

Если мы сейчас откомпилируем приложение и откроем его, то получим следующую недвусмысленную ошибку:

The model backing the 'MvcEfDemoContext' context has changed since the database was created. Consider using Code First Migrations to update the database.

Всё логично – модель поменялась, база – нет. Давайте мигрировать. Запускаем команду для добавления миграции:

PM> Add-Migration Add_Email_to_Test

В качестве параметра передаётся название миграции (к имени файла будет прибавлено слева текущее время). Вы вольны сами выбирать схемы наименования. Например, если хочется сквозной нумерации – называйте миграции в формате “00000”. Лично мне больше нравится вариант с осмысленным названием, потому что порядок уже задаётся текущим временем.

После успешного выполнения этой команды мы получим класс для миграции, который содержит два метода:

public override void Up()
{
AddColumn("Tests", "Email", c => c.String(nullable: false));
}
public override void Down()
{
DropColumn("Tests", "Email");
}

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

Теперь, у нас есть несколько вариантов для запуска миграции:

  • Применить изменения напрямую к базе данных – самый быстрый и простой вариант (для вывода запускаемых команд можно добавить параметр Verbose, доступный, начиная с  версии EF 4.3):
    PM> Update-Database
  • Сгенерировать скрипт (будет открыт в новом окне Visual Studio):
    PM> Update-Database -Script
  • Запустить консольное приложение “Update-Database.exe” (доступное, начиная с версии EF 4.3) по одному из вышеперечисленных сценариев. Этот вариант может быть полезен для автоматических билдов или запуска на сервере клиента, где нет Visual Studio.

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

В нашем случае первая миграция добавит столбец Email:

ALTER TABLE [Tests] ADD [Email] [nvarchar](max) NOT NULL DEFAULT ''

Помимо этого будет создана служебная таблица для хранения информации о миграциях в базе данных.

Если вы запустили команду Update-Database и она завершилась с ошибкой “Cannot open database … requested by the login.” или “Cannot create file '…' because it already exists.”, убедитесь, что в строке соединения указано “Database=aspnetdb.mdf” и в студии не открыто соединение с вашей базой данных.

Корректировка модели

Я нарочно оставил длину Email не указанной, чтобы теперь мы могли посмотреть на то, как происходит миграция при изменении атрибутов свойства. Добавив атрибут StringLength к свойству, снова добавим миграцию и запустим её:

PM> Add-Migration Change_Test_Email_length
PM> Update-Database

С получившимся классом миграции всё в порядке, он содержит следующие методы:

public override void Up()
{
AlterColumn("Tests", "Email", c => c.String(nullable: false, maxLength: 100));
}
public override void Down()
{
AlterColumn("Tests", "Email", c => c.String(nullable: false));
}

А вот после запуска миграции мы получаем ошибку вида:

The object 'DF__Tests__Email__014935CB' is dependent on column 'Email'. ALTER TABLE ALTER COLUMN Email failed because one or more objects access this column.

Здесь у меня претензии к реализации миграций скорее не в том, что изменение столбца “не на полном автомате” а в том, что при создании столбца не назвали DEFAULT CONSTRAINT “DF_Tests_Email”. По этой причине на данном этапе придётся подковыривать миграцию немного больше чем следовало. Получилось так:

public override void Up()
{
RemoveDefaultConstraint();
AlterColumn("Tests", "Email", c => c.String(nullable: false, maxLength: 100));
AddDefaultConstraint();
}
 
public override void Down()
{
RemoveDefaultConstraint();
AlterColumn("Tests", "Email", c => c.String(nullable: false));
AddDefaultConstraint();
}
 
private void RemoveDefaultConstraint()
{
Sql(@"DECLARE @defaultName sysname
SELECT @defaultName = object_name(cdefault) FROM syscolumns
WHERE id = object_id('Tests') AND name = 'Email';
EXEC ('ALTER TABLE Tests DROP CONSTRAINT ' + @defaultName);");
}
 
private void AddDefaultConstraint()
{
Sql(@"ALTER TABLE Tests ADD CONSTRAINT DF_Tests_Email DEFAULT '' FOR Email");
}

Как видите, ничего сверхъестественного, хотя о существующих DEFAULT-ах мог бы позаботиться Entity Framework – хотя бы попробовать пересоздать его в том же виде после изменения столбца – если не получилось бы, тогда уже допиливать руками.

Откат миграции

Теперь (начиная с версии EF 4.3, хотя, возможно, было в предыдущей Beta, которую я не видел) можно откатывать миграции. Сделать это можно с помощью параметра “TargetMigration”, который, надо сказать, работает и в прошлое и в будущее (когда нужно применить к базе данных не все новые миграции).

Update-Database –TargetMigration:Add_Email_to_Test

Проблем при откате не возникло (поскольку метод “Down” я исправил выше вместе с методом “Up”).

Удаление столбца

Чтобы удалить столбец, можно просто пересоздать модель в первоначальном виде (чтобы не править представления) и повторить процесс создания и применения миграции:

PM> Scaffold Model Test -Controller -Force
PM> Add-Migration Remove_Test_Email
PM> Update-Database

Если вы не запустили “Update-Database” после отката миграции, вы не сможете добавить новую, о чём вам сообщат в виде ошибки:

Unable to generate an explicit migration because the following explicit migrations are pending: [201201151649382_Change_Test_Email_length]. Apply the pending explicit migrations before attempting to generate a new explicit migration.

Это без проблем исправляется запуском “Update-Database”, правда в результате её будет предупреждение:

Unable to apply pending changes because automatic migration is disabled. To enable automatic migration, ensure that DbMigrationsConfiguration.AutomaticMigrationsEnabled is set to true.

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

Добавление связанной модели

Чтобы посмотреть на то, как миграции работают со связанными моделями создадим их:

PM> Scaffold Model Test Id,Name,Comment?,CategoryId+ -Controller -Force
PM> Add-Migration Add_Category_to_Test

После добавления миграции она будет выглядеть так:

public override void Up()
{
CreateTable(
"Categories",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(nullable: false, maxLength: 50),
})
.PrimaryKey(t => t.Id);
AddColumn("Tests", "Comment", c => c.String());
AddColumn("Tests", "CategoryId", c => c.Int(nullable: false));
AddForeignKey("Tests", "CategoryId", "Categories", "Id", cascadeDelete: true);
CreateIndex("Tests", "CategoryId");
}
public override void Down()
{
DropIndex("Tests", new[] { "CategoryId" });
DropForeignKey("Tests", "CategoryId", "Categories");
DropColumn("Tests", "CategoryId");
DropColumn("Tests", "Comment");
DropTable("Categories");
}

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

При обновлении базы данных в этом случае будет выдана ошибка, потому что  столбец “CategoryId” будет пытаться заполниться значением 0, а такое значение в таблице “Category” само собой не появится.

Думаю, сейчас вы уже знаете достаточно, чтобы справиться с решением этой проблемы самостоятельно. Дам лишь наводку, что при корректировке миграции доступно не только выполнение SQL напрямую, но и настройки в коде. Например, методу “ColumnBuilder.Int” можно дополнительно передать параметр “defaultValue”.

Автоматические миграции

Скажу немного про уже упомянутые автоматические миграции. Есть возможность использовать режим, когда не нужно добавлять миграции явно с помощью команды “Add-Migration”. Этот режим включается в классе “Configuration” с помощью свойства “AutomaticMigrationsEnabled”.

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

Помимо режима автоматических миграций есть возможность запускать миграции при старте приложения с помощью MigrateDatabaseToLatestVersion database initializer (доступен, начиная с версии EF 4.3).

Что осталось за кадром

Чтобы не раздувать статью я не стал демонстрировать более сложные варианты (изменения связанных таблиц и т.п.). Разумеется, что-то работает автоматом, что-то приходится допиливать.

Также я не уделил внимания следующим нововведениям в EF 4.3:

  • Добавлена достаточно подробная XML-документация к классам миграции.
  • Исключение таблицы “EdmMetadata” – теперь, при первом создании базы данных в стиле Code First без использования миграций, всё равно используются миграции в автоматическом режиме :)
  • Пара исправленных багов (GetDatabaseValues и поддержка Unicode в DbSet).
  • Теперь Code First понимает аннотации для закрытых свойств.
  • Добавлена поддержка конфигурации в config-файле.

Подробности на английском здесь.

Резюме

Пока то, что я вижу в EF 4.3 в плане миграций мне нравится, за исключением некоторых недочётов. Разумеется, попробовать миграции по-настоящему можно только в процессе использования в коммерческой разработке. Буду ждать :)

Если у вас есть замечания, пожелания или новые темы – пишите в комментариях,твиттер или на olegaxenow.reformal.ru. Постараюсь учесть.

11 комментариев:

  1. Появление миграций в EF действительно хорошая новость.

    ОтветитьУдалить
  2. А почему оно решило, что если поле не nullable, то надо навесить дефолт констрейт со сгенеренным именем и пустым значением? Уж лучше бы они EF как ORM пытались хоть как-то к NH приблизить :(

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

    Использую Migrator.Net - он решает все инфраструктурные вопросы, а на разработчика остается ток код миграции самой.

    ОтветитьУдалить
  3. To @Vasiliy Shiryaev:

    А что по вашему должно было произойти, с учётом того, что в таблице есть данные? К слову сказать, никто не мешает подправить код миграции перед применением.

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

    Поэтому не совсем понятно, в чём преимущество Migrator.Net - в том, что не предлагает решения для простых случаев? ;)

    ОтветитьУдалить
  4. Что должно было произойти? Это зависит от от ситуации. Скорее всего не требовать not null вообще, ибо навешивание пустой строчки по умолчанию вместо null поля более, чем странно, имхо.
    Второй вопрос вы уже сами озвучили по поводу имени констрейнта, и добавили к этому костыль.

    Да, проверять можно, так же и SQL можно вставить, согласен. Но во-первых, на проверку требуется больше усилий, чем на написание( не реальную проверку с анализом последствий каждой строчки, а не просто беглый просмотр, что все вроде бы в порядке ). А во-вторых, это не "можно", это превратится в "нужно".

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

    ОтветитьУдалить
  5. To @Vasiliy Shiryaev
    > ибо навешивание пустой строчки по умолчанию вместо null поля более, чем странно, имхо.

    Теперь я понял в чём у нас была рассинхронизация :)
    Для свойства с атрибутом *Required* - не странно.

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

    ОтветитьУдалить
  6. А по-моему, все-таки странно - *Required* означает, что поле обязано иметь значение. Пустое значение - это как-то странно в контексте *Required*, но приемлимо - если у вас нету вариантов больше для текущих данных. А вот констрейнт по умолчанию для новых данных точно лишний.
    Тут мигратор должен был сделать что-то такое:
    - создать поле null
    - установить все строчки в значение по умолчанию
    - обновить поле на not null.

    Остальной текст касался миграторов вообще - ну не верю я в них:) Начиная с проектов типа База данных в студии 2005ой, за которым нужно было глаз да глаз, до вот мигратора на основе модели.

    ОтветитьУдалить
  7. To @Vasiliy Shiryaev:
    >Тут мигратор должен был сделать что-то такое...

    Это, что называется, на вкус и цвет.
    Меня в подходе с созданием default constraint расстраивает больше то, что ему не дали нормального названия.
    Напоминаю что, при желании, в миграции легко можно задать своё значение по умолчанию без использования SQL-кода.

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

    ОтветитьУдалить
  8. А как в code first использовать хранимые процедуры?

    ОтветитьУдалить
  9. Добрый день. Видимо в описании примера опущены какие то необходимые детали.
    Созданный context не содержит context.Test, которую генерирует визард. При исправлении на context.Tests, не работает ниже прведенный контролер

    public class TestController : Controller
    {
    private MyMvc3Context context = new MyMvc3Context();
    public ViewResult Index()
    {
    return View(context.Tests.ToList());

    Генерируеттся ошибка: The type 'Test' is not attributed with EdmEntityTypeAttribute but is contained in an assembly attributed with EdmSchemaAttribute. POCO entities that do not use EdmEntityTypeAttribute cannot be contained in the same assembly as non-POCO entities that use EdmEntityTypeAttribute.

    ОтветитьУдалить
  10. To Анонимный:
    >А как в code first использовать хранимые процедуры?

    Вопрос про использование процедур вообще? Тогда можно использовать такой вариант:
    http://stackoverflow.com/questions/4845246/does-entity-framework-code-first-support-stored-procedures

    Миграций процедур вроде бы не предусмотрено.

    To @Alex Fatkin:
    В проекте есть edmx-файлы? Code first с ними не дружит.

    ОтветитьУдалить
  11. Чуть не забыл, если нужны процедуры в Code First с типизацией, можно попробовать такой подход: http://www.codeproject.com/Articles/179481/Code-First-Stored-Procedures

    ОтветитьУдалить