Поделиться через


Code First Migrations

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

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

Следующее руководство содержит обзор возможности Code First Migrations в Entity Framework. Вы можете выполнить все пошаговое руководство или сразу перейти в нужный раздел. Рассмотрены следующие вопросы:

Создание начальной модели и базы данных

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

  • Создайте новое консольное приложение MigrationsDemo.
  • Добавьте последнюю версию пакета NuGet EntityFramework в проект
    • Средства —> Диспетчер пакетов библиотеки —> Консоль диспетчера пакетов
    • Запустите команду EntityFramework Install-Package
  • Добавьте файл Model.cs с кодом, показанным ниже. Этот код определяет один класс Blog, составляющий нашу модель предметной области, и класс BlogContext, который будет нашим контекстом EF Code First.
    using System.Data.Entity;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Data.Entity.Infrastructure;

    namespace MigrationsDemo
    {
        public class BlogContext : DbContext
        {
            public DbSet<Blog> Blogs { get; set; }
        }

        public class Blog
        {
            public int BlogId { get; set; }
            public string Name { get; set; }
        }
    }
  • Теперь, когда у нас есть модель, пора использовать ее для доступа к данным. Внесите в файл Program.cs приведенный ниже код.
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;

    namespace MigrationsDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                using (var db = new BlogContext())
                {
                    db.Blogs.Add(new Blog { Name = "Another Blog " });
                    db.SaveChanges();

                    foreach (var blog in db.Blogs)
                    {
                        Console.WriteLine(blog.Name);
                    }
                }

                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
            }
        }
    }
  • Запустите приложение, и вы увидите, что база данных MigrationsCodeDemo.BlogContext создается автоматически.

    База данных LocalDB

Активирование миграций

Пора внести дополнительные изменения в нашу модель.

  • Давайте внесем в класс Blog свойство Url.
    public string Url { get; set; }

Если вы снова запустите приложение, вы получите InvalidOperationException, заявив, что модель, лежащая в основе контекста 'BlogContext', изменилась с момента создания базы данных. Рекомендуется использовать Code First Migrations для обновления базы данных (http://go.microsoft.com/fwlink/?LinkId=238269).

Как видно из исключения, пора приступить к использованию Code First Migrations. Сначала нужно активировать миграции в нашем контексте.

  • Запустите команду Enable-Migrations в консоли диспетчера пакетов.

    В результате этой команды в проект будет добавлена папка Migrations. В новой папке два файла:

  • Класс конфигурации. Этот класс позволяет настраивать поведение миграций для контекста. В этом пошаговом руководстве мы будем просто использовать конфигурацию по умолчанию. Поскольку имеется только один контекст Code First в проекте, Enable-Migrations автоматически заполняет тип контекста, к которому относится эта конфигурация.

  • Миграция "InitialCreate". Эта миграция создана, так как мы уже использовали Code First для создания базы данных, прежде чем включили миграции. Код в этой сгенерированной миграции представляет объекты, которые уже были созданы в базе данных. В нашем случае это таблица Blog со столбцами BlogId и Name. Имя файла содержит метку времени для удобства упорядочения. Если бы база данных еще не была создана, миграция InitialCreate не была бы добавлена в проект. Вместо этого, когда мы впервые вызвали бы Add-Migration, код для создания этих таблиц был бы перенесен в новую миграцию.

Несколько моделей для одной целевой базы данных

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

Начиная с EF6, класс Configuration включает свойство ContextKey. Это уникальный идентификатор для каждой модели Code First. В соответствующий столбец в таблице __MigrationsHistory вносятся записи из нескольких моделей, которые используют таблицу совместно. По умолчанию этому свойству присвоено полное имя контекста.

Создание и запуск миграций

Вы познакомитесь с двумя основными командами Code First Migrations.

  • Add-Migration сформирует следующую миграцию на основе изменений, внесённые в модель с момента создания последней миграции.
  • Update-Database будет применять ожидающие миграции к базе данных.

Нам нужно создать основу для миграции, чтобы поддерживать новое свойство URL. Команда Add-Migration позволяет нам давать этим миграциям имя. Давайте назовем нашу AddBlogUrl.

  • Запустите команду Add-Migration AddBlogUrl в консоли диспетчера пакетов.
  • В папке Migrations у нас теперь есть новая миграция AddBlogUrl. Имя файла миграции имеет метку времени в целях упорядочения.
    namespace MigrationsDemo.Migrations
    {
        using System;
        using System.Data.Entity.Migrations;

        public partial class AddBlogUrl : DbMigration
        {
            public override void Up()
            {
                AddColumn("dbo.Blogs", "Url", c => c.String());
            }

            public override void Down()
            {
                DropColumn("dbo.Blogs", "Url");
            }
        }
    }

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

  • Запустите команду Update-Database в консоли диспетчера пакетов.
  • Code First Migrations сравнит миграции из папки Migrations с теми, которые были применены к базе данных. Вы увидите, что необходимо применить и запустить миграцию AddBlogUrl.

База данных MigrationsDemo.BlogContext обновлена и включает столбец Url в таблице Blog.

Настройка миграции

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

  • Пора внести некоторые изменения в нашу модель. Давайте добавим новое свойство Rating в класс Blog.
    public int Rating { get; set; }
  • Также добавим новый класс Post.
    public class Post
    {
        public int PostId { get; set; }
        [MaxLength(200)]
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
  • Еще мы добавим коллекцию Posts в класс Blog, чтобы создать второй конец связи между Blog и Post.
    public virtual List<Post> Posts { get; set; }

Мы будем использовать команду Add-Migration, чтобы Code First Migrations создал предположительный шаблон миграции для нас. Мы назовем эту миграцию AddPostClass.

  • Запустите команду Add-Migration AddPostClass в консоли диспетчера пакетов.

С помощью Code First Migrations удалось довольно хорошо подготовить эти изменения, но есть кое-что, что мы, возможно, хотим изменить.

  1. Сначала давайте добавим уникальный индекс в столбец Posts.Title (строки 22 и 29 в приведенном ниже коде).
  2. Мы также добавим столбец Blogs.Rating, не допускающий значение null. Если в таблице есть данные, новому столбцу будет назначено значение CLR по умолчанию в зависимости от типа данных (оценка — это целое число, так что это будет 0). Но нам нужно указать значение по умолчанию 3, чтобы существующие строки в таблице Blogs начинались с неплохой оценки. (Значение по умолчанию присваивается в строке 24 приведенного ниже кода.)
    namespace MigrationsDemo.Migrations
    {
        using System;
        using System.Data.Entity.Migrations;

        public partial class AddPostClass : DbMigration
        {
            public override void Up()
            {
                CreateTable(
                    "dbo.Posts",
                    c => new
                        {
                            PostId = c.Int(nullable: false, identity: true),
                            Title = c.String(maxLength: 200),
                            Content = c.String(),
                            BlogId = c.Int(nullable: false),
                        })
                    .PrimaryKey(t => t.PostId)
                    .ForeignKey("dbo.Blogs", t => t.BlogId, cascadeDelete: true)
                    .Index(t => t.BlogId)
                    .Index(p => p.Title, unique: true);

                AddColumn("dbo.Blogs", "Rating", c => c.Int(nullable: false, defaultValue: 3));
            }

            public override void Down()
            {
                DropIndex("dbo.Posts", new[] { "Title" });
                DropIndex("dbo.Posts", new[] { "BlogId" });
                DropForeignKey("dbo.Posts", "BlogId", "dbo.Blogs");
                DropColumn("dbo.Blogs", "Rating");
                DropTable("dbo.Posts");
            }
        }
    }

Наша измененная миграция готова к запуску, так что давайте воспользуемся командой Update-Database, чтобы обновить базу данных. Давайте укажем флаг –Verbose, чтобы вы могли видеть SQL, который выполняет Code First Migrations.

  • Запустите команду Update-Database –Verbose в консоли диспетчера пакетов.

Перемещение данных или настраиваемый SQL

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

  • Давайте добавим свойство Post.Abstract в нашу модель. Позже мы заполним свойство Abstract для существующих записей текстом из начала столбца Content.
    public string Abstract { get; set; }

Мы будем использовать команду Add-Migration, чтобы Code First Migrations автоматически сгенерировала наиболее вероятный вариант миграции для нас.

  • Запустите команду Add-Migration AddPostAbstract в консоли диспетчера пакетов.
  • Созданная миграция обрабатывает изменения схемы, но мы также хотим предварительно заполнить столбец Abstract первыми 100 символами данных для каждой публикации. Для этого мы можем перейти к SQL и выполнить оператор UPDATE после добавления столбца. (Добавление в строке 12 в приведенном ниже коде)
    namespace MigrationsDemo.Migrations
    {
        using System;
        using System.Data.Entity.Migrations;

        public partial class AddPostAbstract : DbMigration
        {
            public override void Up()
            {
                AddColumn("dbo.Posts", "Abstract", c => c.String());

                Sql("UPDATE dbo.Posts SET Abstract = LEFT(Content, 100) WHERE Abstract IS NULL");
            }

            public override void Down()
            {
                DropColumn("dbo.Posts", "Abstract");
            }
        }
    }

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

  • Запустите команду Update-Database –Verbose в консоли диспетчера пакетов.

Миграция на определенную версию (включая понижение версии)

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

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

  • Запустите команду Update-Database –TargetMigration: AddBlogUrl в консоли диспетчера пакетов.

Эта команда запустит скрипт понижения версии для миграций AddBlogAbstract и AddPostClass.

Если вы хотите выполнить откат до пустой базы данных, используйте команду Update-Database –TargetMigration: $InitialDatabase.

Получение скрипта SQL

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

  • Запустите команду Update-Database, но на этот раз укажите флаг –Script, чтобы записать изменения в скрипт, а не применить. Мы также укажем исходную и целевую миграцию, для которой необходимо создать скрипт. Мы хотим, чтобы скрипт переходил от пустой базы данных ($InitialDatabase) до последней версии (миграция AddPostAbstract). Если вы не укажете целевую миграцию, будет использована последняя миграция в качестве цели. Если вы не укажете исходные миграции, система Migrations будет использовать текущее состояние базы данных.
  • Запустите команду Update-Database -Script -SourceMigration: $InitialDatabase -TargetMigration: AddPostAbstract в консоли диспетчера пакетов.

Code First Migrations будет выполнять конвейер миграций, но будет не применять изменения, а записывать их в SQL-файл. Созданный скрипт автоматически открывается в Visual Studio, готовый для просмотра или сохранения.

Создание идемпотентных скриптов

Начиная с EF6, если вы укажете –SourceMigration $InitialDatabase, создаваемый скрипт будет идемпотентным. Идемпотентные скрипты могут обновить базу данных любой версии до последней версии (или указанной версии, если вы используете –TargetMigration). Созданный скрипт содержит логику для проверки таблицы __MigrationsHistory и применяет только те изменения, которые еще не применены.

Автоматическое обновление при запуске приложения (инициализатор MigrateDatabaseToLatestVersion)

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

Мы можем обновить файл Program.cs, как показано ниже, чтобы настроить инициализатор MigrateDatabaseToLatestVersion для BlogContext, прежде чем мы используем контекст (строка 14). Обратите внимание, что необходимо также добавить оператор using для пространства имен System.Data.Entity (строка 5).

При создании экземпляра этого инициализатора нам нужно указать тип контекста (BlogContext) и конфигурацию миграций (Configuration). Конфигурация миграций — это класс, который был добавлен в нашу папку Migrations при включении Migrations.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Data.Entity;
    using MigrationsDemo.Migrations;

    namespace MigrationsDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                Database.SetInitializer(new MigrateDatabaseToLatestVersion<BlogContext, Configuration>());

                using (var db = new BlogContext())
                {
                    db.Blogs.Add(new Blog { Name = "Another Blog " });
                    db.SaveChanges();

                    foreach (var blog in db.Blogs)
                    {
                        Console.WriteLine(blog.Name);
                    }
                }

                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
            }
        }
    }

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