Entity Framework 5 Release Candidate Code First: Механизм миграции
Сложность 200.
Видимо настал тот момент, когда можно обратиться к Entity Framework, тем более что поступающая информация все сильнее и сильнее радует меня и EF обретает любопытные черты и свойства. Пятая версия фреймворка, в моих глазах уже может претендовать на то, чтобы появиться в боевой среде.
Более всего меня увлекает на данный момент подход CodeFirst, с возможностью миграций, т.е. поддержкой версионности базы. Очевидно на многих, достаточно простых сценариях, миграции отрабатывают хорошо, генерируя скрипты миграции вверх и вниз. Что весьма и весьма полезно. Однако нашлась ложка дегтя в этой бочке меда, либо я чего-то специфического не знаю, о чем все молчат =). Но обо всем по порядку.
Подготовка
Нам понадобится база данных MS SQL. В моем случае это SQL Server 2012, вы можете поставить себе Express версию, хотя скорее всего она уже есть у вас. Далее понадобится .net framework не ниже 4 версии. Конечно же Visual Studio и менеджер пакетов NuGet.
Начнем с создания простого консольного приложения, так как нам будет важен сам факт сохранения и чтения данных.
Вводная информация по EF
Немного информации о том, как работать при подходе Code First, если кто-то только что услышал об этом. Исходя из самого названия и полученной ранее информации вы можете догадаться, что база данных строится исходя из кода приложения, тех классов что мы укажем. Минимальный набор классов для работы это: DbContext и DbSet. Первый класс будет родительским для определения контекста базы, второй будет представлять коллекции, которые отображаются на таблицы базы. Может немного путанно сейчас, но на практике будет понятнее. Сейчас мы к ней перейдем.
Итак, у нас есть пустое приложение и необходимо добавить ссылку на последнюю экспериментальную версию EF5, для чего мы воспользуемся NuGet:
PS > Install-Package EntityFramework –Pre
Главное не забыть параметр Pre! Через некоторое время скачаются все необходимые библиотеки, добавятся ссылки и можно будет начать использовать EF.
Создадим подопытный класс, традиционно это у нас будет Customer.
1 2 3 4 5 6 7 8 |
public class Customer { public Guid Id { get; set; } public string Name { get; set; } public Customer() { Id = Guid.NewGuid(); } } |
Тут требуются некоторые пояснения. Начиная с пятой версии EF может использовать в качестве первичного ключа Guid, а первичным ключом по умолчанию считается свойство с именем Id. Если хочется задать другое имя, то на помощь придет атрибут KeyAttribute.
Для работы по восстановлению данных для EF требуется конструктор без параметров, иначе магии не получится. За нас EF не проставит значение первичного ключа, так что мы его инициализируем в конструкторе. Но это и к лучшему.
Следующим шагом будет создание контекста базы. Для этого потребуется создать класс и наследовать его от DbContext. Далее можно будет воспользоваться конструкторами базового класса для детального определения строки соединения. Воспользуемся непривычным для меня приемом и определим строку соединения в файле конфигурации, раз уж он все равно добавился в проект.
1 2 3 4 5 |
<connectionStrings> <add name="violet-tape" connectionString="Data Source=lethiathan\lth;Initial Catalog=Migration;Integrated Security=True" providerName="System.Data.SqlClient"/> </connectionStrings> |
Далее можно определить контекст базы данных для работы:
1 2 3 4 5 |
public class Context : DbContext { public Context() : base("violet-tape") {} public DbSet<Customer> Customers { get; set; } } |
Собственно это и есть все определение контекста, по которому будет создана база. Осталось только задействовать все это вместе. Например, следующим образом:
1 2 3 4 5 6 7 8 9 10 |
internal class Program { private static void Main(string[] args) { var context = new Context(); var customer = new Customer {Name = "ACME"}; context.Customers.Add(customer); context.SaveChanges(); Console.WriteLine("Database created {0}", context.Database.Exists()); } } |
Последняя строчка только для развлекательных целей, чтобы убедиться, что база создалась. Хотя в этом можно будет убедиться подключившись к ней, и посмотрев, что за данные пришли в базу. Итак, запускаем программу. Все отработало хорошо, и можно обратиться к SSMS.
Вот что можно наблюдать в SSMS:
Тут информация с небольшим забеганием вперед, ну да ничего страшного, сейчас все и расскажу. Как можно видеть в Object Explorer есть база Migration, верьте мне, что она создалась, а не была тут с начала веков. В этой базе есть наша таблица Customers, в которой содержатся указанные нами данные. Так же интересной таблицей для нас будет __MigrationHistory, в которой содержится имя версии базы, слепок модели, время создания, а так же версия EF. Данная таблица присутствует в базе несмотря на то, что мы еще не включали механизм миграций, который сам по себе не включится. Но дальше будет видно, что все наши изменения базы отображаются в этой таблице и можно всегда точно и с уверенностью сказать в каком состоянии находится схема данных базы.
Для закрепления материала, посмотрим, что чтение данных так же не вызывает никаких проблем:
1 2 3 4 5 6 7 8 9 10 |
internal class Program { private static void Main(string[] args) { var context = new Context(); foreach (var customer in context.Customers) { Console.WriteLine(customer.Name); } Console.WriteLine("Database exists {0}", context.Database.Exists()); } } |
Миграции
Итак, у нас есть программа, будем думать что она большая будет когда-то, а сейчас был первый релиз и на подходе следующая версия в которой мы добавим к классу Customer новое поле. Но прежде чем добавлять поле включим возможность миграций. Для этого потребуется в Package Manager Console набрать:
PS > Enable-Migrations
После чего получим такой вот класс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
internal sealed class Configuration : DbMigrationsConfiguration<Context> { public Configuration() { AutomaticMigrationsEnabled = false; } protected override void Seed(Context context) { // This method will be called after migrating to the latest version. // You can use the DbSet<T>.AddOrUpdate() helper extension method // to avoid creating duplicate seed data. E.g. // // context.People.AddOrUpdate( // p => p.FullName, // new Person { FullName = "Andrew Peters" }, // new Person { FullName = "Brice Lambson" }, // new Person { FullName = "Rowan Miller" } // ); // } } |
В новой папке Migrations, которая должна появится в вашем проекте. В нее будут складываться все созданные миграции. Сейчас выключены автоматические миграции, их можно будет включить вручную, но мы пока что не будем этого делать.
Создание кода миграции
Итак, миграции включены. Можно править классы.
1 2 3 4 5 6 7 8 9 |
public class Customer { public Guid Id { get; set; } public string Name { get; set; } public int Index { get; set; } public Customer() { Id = Guid.NewGuid(); } } |
После этого в консоли NuGet надо будет запустить команду Add-Migration ‘ИмяМиграции’:
PM> Add-Migration IndexAdded
Результатом команды будет новый класс миграции IndexAdded:
1 2 3 4 5 6 7 8 9 |
public partial class IndexAdded : DbMigration { public override void Up() { AddColumn("Customers", "Index", c => c.Int(nullable: false)); } public override void Down() { DropColumn("Customers", "Index"); } } |
Конечно это достаточно легкий случай и по этому судить нельзя, однако можно посмотреть, какие еще служебные методы предлагает нам базовый класс DbMigration
В общем много самых разных, даже переименование колонок. Для выполнения произвольных SQL скриптов существует метод Sql(). К нему мы еще вернемся.
Итак, у нас есть код для обновления, теперь осталось его запустить. Делается это с помощью команды Update-Database.
PS > Update-Database
После чего в консоли должны увидеть, как перечисляются все скрипты, которые применятся к указанной базе данных. Можно снова взглянуть на базу, на то, что с ней стало.
Видим, что теперь в истории базы две записи, а в таблице Customers появилась новая колонка. Замечательно!
Попробуем теперь сделать переименование колонки, обычно во всяких системах автоматической миграции и генерации скриптов это происходит через drop\create, что приведет к потере данных, чего мы, конечно же, не хотим. Но посмотрим, что тут у нас получится. Сменим свойство Name на Title.
1 2 3 4 5 6 7 8 9 |
public class Customer { public Guid Id { get; set; } public string Title { get; set; } public int Index { get; set; } public Customer() { Id = Guid.NewGuid(); } } |
Создаем новый скрипт миграции с помощью уже известной нам команды:
PS > Add-Migration NameToTitle
Получаем снова новый класс с указанным нами именем и видим, что код создан по принципу drop\create, но мы можем все исправить!
1 2 3 4 5 6 7 8 9 10 11 |
public partial class NameToTitle : DbMigration { public override void Up() { AddColumn("Customers", "Title", c => c.String()); DropColumn("Customers", "Name"); } public override void Down() { AddColumn("Customers", "Name", c => c.String()); DropColumn("Customers", "Title"); } } |
Мы знаем что это простое переименование и можем воспользоваться методом RenameColumn() или же написать произвольный SQL код. Давайте попробуем второй вариант.
1 2 3 4 5 6 7 8 9 |
public partial class NameToTitle : DbMigration { public override void Up() { Sql("exec sp_rename 'Customers.Name', 'Title', 'Column'"); } public override void Down() { Sql("exec sp_rename 'Customers.Title', 'Name', 'Column'"); } } |
Выглядит убедительно на данный момент, попробуем обновить базу и прочитать данные после таких изменений.
PS > Update-Database
Пробуем запустить приложение и получить данные. Все успешно запустилось.
Отлично, в целом миграции вверх работают, я пробовал и более сложные сценарии все в целом отрабатывалось хорошо, за исключением определенной последовательности действий, которые мы обсудим чуть позже.
Миграция к предыдущим версиям
Для миграции вниз, используется та же команда Update-Database, но указываются дополнительные параметры к какой версии базы необходимо привести текущую версию. Вернемся к предыдущей версии базы:
PS > Update-Database -TargetMigration:IndexAdded
Все должно пройти без ошибок, и посмотрим что там с базой стало:
Все вернулось в нужному виду. Вернув код класса Customer к предыдущему виду, все снова заработало.
Миграция вниз поддерживается только с последней версии базы, что логично, так как миграция вниз с произвольной до произвольной версии может легко привести базу в несогласованный вид.
Косяки
UPD: описанную ниже проблему удалось разрешить удачно. Однако я надеюсь что в релизе описанные сценарий не будет приводить к шаманским пляскам. Решение опишу в следующем посте.
Теперь можно рассмотреть действия, при которых EF отказывается воспринимать изменения, что удивительно на мой взгляд.
Итак, приведем базу к последней версии и не меняя класс Customer (в смысле вернуть его к виду где есть Title, но новых свойств нет), создадим новый класс миграции, который назовем AddCreateStamp. Т.е.
PS > Update-Database
PS > Add-Migration AddCreateStamp
После указанных действий у вас должен появиться новый класс с пустой миграцией
1 2 3 4 5 |
public partial class AddCreateStamp : DbMigration { public override void Up() {} public override void Down() {} } |
Попробуем добавить новую колонку в таблицу и новое поле в класс Customer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public partial class AddCreateStamp : DbMigration { public override void Up() { AddColumn("Customers","CreateDate", x => x.DateTime(nullable:true)); } public override void Down() { DropColumn("Customers", "CreateDate"); } } public class Customer { public Guid Id { get; set; } public string Title { get; set; } public int Index { get; set; } public DateTime CreateDate { get; set; } public Customer() { Id = Guid.NewGuid(); CreateDate = DateTime.Now; } } |
Отлично, все более чем похоже на правду и пробуем обновить базу данных.
PM> Update-Database
1 2 3 4 5 6 7 |
Specify the '-Verbose' flag to view the SQL statements being applied to the target database. Applying explicit migrations: [201206060436241_AddCreateStamp]. Applying explicit migration: 201206060436241_AddCreateStamp. Unable to update database to match the current model because there are pending changes and automatic migration is disabled. Either write the pending model changes to a code-based migration or enable automatic migration. Set DbMigrationsConfiguration.AutomaticMigrationsEnabled to true to enable automatic migration. You can use the Add-Migration command to write the pending model changes to a code-based migration. |
Появилось такое предупреждение, но база при этом обновилась.
Но при этом слепок модели не обновился, в чем можно убедиться, выполнив нехитрый скрипт
1 |
select distinct model from [dbo].[__MigrationHistory] |
Три строки всего, и как результат мы не сможем прочитать данные из базы, так как по мнению EF модель и база не совпадают.
И сколько бы мы не обновляли базу, ничего не поможет.
Ок, если почитать внимательно предыдущее сообщение о обновлении, то нам рекомендовали включить автоматические обновления. Попробуем.
1 2 3 4 5 6 |
internal sealed class Configuration : DbMigrationsConfiguration<Context> { public Configuration() { AutomaticMigrationsEnabled = true; } ... } |
PM> Update-Database
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Specify the '-Verbose' flag to view the SQL statements being applied to the target database. No pending explicit migrations. Applying automatic migration: 201206060456160_AutomaticMigration. System.Data.SqlClient.SqlException (0x80131904): Column names in each table must be unique. Column name 'CreateDate' in table 'Customers' is specified more than once. … System.Data.Entity.Migrations.Infrastructure.MigratorLoggingDecorator.Upgrade(IEnumerable`1 pendingMigrations, String targetMigrationId, String lastMigrationId) at System.Data.Entity.Migrations.DbMigrator.Update(String targetMigration) at System.Data.Entity.Migrations.Infrastructure.MigratorBase.Update(String targetMigration) at System.Data.Entity.Migrations.Design.ToolingFacade.UpdateRunner.RunCore() at System.Data.Entity.Migrations.Design.ToolingFacade.BaseRunner.Run() ClientConnectionId:8f487310-73b3-49f5-9318-ba95b0d019b6 Column names in each table must be unique. Column name 'CreateDate' in table 'Customers' is specified more than once. |
Вот такая оказия.
Как это победить я пока что не знаю, но явно должен быть способ.
С одной стороны можно принять идею что «не делайте так», с другой стороны любая миграция должна учитывать изменения и генерировать новый слепок модели. Если кто знает как это сделать – дайте знать, очень интересно!
В остальном весьма приятное впечатление от Code First и механизма миграций.
Итого
Я попытался рассказать только о механизме миграции не вдаваясь в подробности других тем: особенностей создания баз данных, использование атрибутов и тонкая настройка модели, и много чего еще.
В общем надо пробовать и стараться приспособить к небольшим проектам или проектам, для которых приемлем высокий уровень рисков.
И да, надо бы покопаться в кишках EF, поискать побольше инфо о внутреннем устройстве, дабы понимать, откуда растут указанные ограничения и каковы возможности.
Hard’n’heavy!
Я че т не понял в чем криминал с колонкой CreateDate… Почему не пркоанало? И как именно делать нельзя?
Да, в статье «EF5 RF CF — Force Recovery Mode» есть детальный ответ и рассмотрение случая.
Вопрос можно?
Насколько я понял наряду с DropCreateDatabaseIfModelChanges, DropCreateDatabaseAlways и CreateDatabaseIfNotExists появился еще один класс MigrateDatabaseToLatestVersion.
Если мой Initializer будет наследником этого класса будет ли при первом запуске приложения создана пустая БД?