Миграции в Entity Framework Core

Внимание

Данный материал является частью цикла статей «Основы Entity Framework Core». Не забудьте посмотреть другие статьи по этой теме :-)

  1. Начало работы с Entity Framework Core в ASP.NET Core - модели, DbContext, конфигурация
  2. Настройка нереляционных свойств в EF Core
  3. Миграции в Entity Framework Core
  4. Entity Framework Core – организация связей между моделями: соглашения, аннотации данных и Fluent API
  5. Запросы к базе данных в Entity Framework Core
  6. Изменение данных с помощью Entity Framework Core

Создание и применение миграций в EF Core

Использование миграции - это стандартный способ создания и обновления базы данных с помощью Entity Framework Core. Процесс миграции состоит из двух этапов: создание миграции и применение миграции. Как мы уже говорили, схема нашей базы данных должна быть согласована с моделью базы данных, и каждое изменение в модели базы данных необходимо переносить в саму базу данных.

Эти изменения могут быть, например, следующими:

  • Изменения в свойствах класса модели
  • Изменения конфигурации
  • Добавление или удаление свойств DbSet<T> из класса контекста

Начиная с ASP.NET Core 3.0 инструменты EF Core, необходимые для миграции, не устанавливаются предварительно. Следовательно, мы должны установить библиотеку Microsoft.EntityFrameworkCore.Tools. Если вы следите за этой серией с самого начала, значит, у вас уже установлена ​​библиотека Microsoft.EntityFrameworkCore.

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

Добавление миграции Add-Migration MigrationName [options]

Или через интерфейс командной строки dotnet:

dotnet ef migrations добавить MigrationName [options]

В нашем приложении мы собираемся использовать Консоль диспетчера пакетов (PMC), поэтому давайте сделаем это, набрав:

PM> Add-Migration InitialMigration

После того, как мы нажмем клавишу Enter, наша миграция будет завершена.

Действия, которые происходят за кулисами

Файл ApplicationContextModelSnapshot.cs содержит модель базы данных и обновляется каждый раз при добавлении новой миграции. Два других файла: InitialMigration и InitialMigration.Designer - это файлы, содержащие и описывающие вновь созданную миграцию.

Итак, если вы выполнили все шаги из предыдущих статей, содержимое файла InitialMigration должно выглядеть следующим образом:

public partial class InitialMigration : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Student",
            columns: table => new
            {
                StudentId = table.Column<Guid>(nullable: false),
                Name = table.Column<string>(maxLength: 50, nullable: false),
                Age = table.Column<int>(nullable: true),
                IsRegularStudent = table.Column<bool>(nullable: false, defaultValue: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Student", x => x.StudentId);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Student");
    }
}

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

Применение созданной миграции

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

Для консоли диспетчера пакетов команда:

Update-Database [options]

Для окна командной строки команда:

dotnet ef database update [options]

Поскольку мы уже определились с PMC, давайте откроем окно PMC и выполним команду:

PM> Update-Database

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

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

Но как EF Core узнает, какую миграцию нужно применить?

Ну, он сохраняет уникальный идентификатор в _EFMigrationsHistory, который представляет собой уникальное имя файла миграции, созданного при миграции, и никогда больше не выполняет файлы с тем же именем:

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

Добавление собственного кода в файл миграции

Мы уже объяснили назначение методов Up и Down в нашем файле InitialMigration. Но весь код в этих методах генерируется EF Core. При необходимости мы также можем добавить наш собственный код. Мы можем использовать параметр MigrationBuilder для доступа к широкому спектру методов, которые могут помочь нам в этом процессе. Одним из таких методов является метод Sql, который мы можем использовать для добавления желаемого пользовательского кода.

Итак, давайте откроем класс InitialMigration и изменим его, добавив наш собственный код:

public partial class InitialMigration : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Student",
            columns: table => new
            {
                StudentId = table.Column<Guid>(nullable: false),
                Name = table.Column<string>(maxLength: 50, nullable: false),
                Age = table.Column<int>(nullable: true),
                IsRegularStudent = table.Column<bool>(nullable: false, defaultValue: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Student", x => x.StudentId);
            });

        migrationBuilder.Sql(@"CREATE PROCEDURE MyCustomProcedure
                               AS
                               SELECT * FROM Student");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Student");

        migrationBuilder.Sql(@"DROP PROCEDURE MyCustomProcedure");
    }
}

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

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

Создание миграции, если объекты и файлы Dbcontext находятся в отдельном проекте

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

Попробуем продемонстрировать, что мы имеем в виду.

Первым делом необходимо создать еще один проект библиотеки классов .NET Core и назвать его Entities и установить Microsoft.EntityFrameworkCore и Microsoft.EntityFrameworkCore.Relational. пакеты через диспетчер пакетов NuGet или окно PMC:

PM> Install-Package Microsoft.EntityFrameworkCore -Version 3.1.0

PM> Install-Package Microsoft.EntityFrameworkCore.Relational -Version 3.1.0

Затем нам нужно добавить ссылку на проект Entities в нашем основном проекте.

Как только мы это сделаем, нам нужно изменить пространство имен в классах ApplicationContext и Student с EFCoreApp.Entities на просто < код>Сущности. Кроме того, мы должны сделать то же самое для директив using в классе Startup и в всех трех файлах миграции.

После всего этого наш проект должен успешно собраться.

Добавление новой миграции

Теперь мы можем попробовать добавить еще одну миграцию, набрав:

PM> Add-Migration TestMigrationFromSeparateProject

Но как только мы нажмем клавишу Enter, мы получим сообщение об ошибке, в котором объясняется, что наш проект EFCoreApp не соответствует нашим объектам сборки миграции. Это сообщение об ошибке замечательно, потому что оно дает нам объяснение, как решить нашу проблему.

Все, что нам нужно сделать, это изменить нашу сборку миграции, поэтому давайте сделаем именно это в классе Startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationContext>(opts =>
        opts.UseSqlServer(Configuration.GetConnectionString("sqlConnection"),
            options => options.MigrationsAssembly("EFCoreApp")));

    services.AddControllers();
}

Теперь мы можем снова запустить ту же команду, но на этот раз она будет выполнена успешно. Мы успешно создали нашу новую миграцию вместе с файлами миграции в папке Migrations:

Мы видим, что в файле TestMigration нет кода в методах Up и Down, и это нормально, потому что мы не изменить что-либо, но мы выполнили требуемую задачу.

Удаление миграции

Мы узнали, как создавать миграции из отдельного проекта. Но в результате мы создали пустую миграцию, которая ничего не делает в нашей базе данных. Когда мы создаем миграцию, которая нас не устраивает, мы можем легко удалить ее, набрав команду Remove-Migration [options] в окне PMC. Итак, давайте сделаем это:

PM> Remove-Migration

Через несколько секунд наша предыдущая миграция будет удалена:

Отлично, теперь мы можем двигаться дальше.

Исходные данные в Entity Framework Core

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

Мы можем создать код для действия заполнения в методе OnModelCreating с помощью ModelBuilder, как мы это сделали для конфигурации Fluent API. Итак, давайте добавим несколько строк в таблицу Student:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>()
        .ToTable("Student");
    modelBuilder.Entity<Student>()
        .Property(s => s.Age)
        .IsRequired(false);
    modelBuilder.Entity<Student>()
        .Property(s => s.IsRegularStudent)
        .HasDefaultValue(true);

    modelBuilder.Entity<Student>()
        .HasData(
            new Student
            {
                Id = Guid.NewGuid(),
                Name = "John Doe",
                Age = 30
            },
            new Student
            {
                Id = Guid.NewGuid(),
                Name = "Jane Doe",
                Age = 25
            }
        );
}

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

Теперь мы можем создать новую миграцию:

PM> Add-Migration SeedInitialData

И примените его:

PM> Update-Database

Мы можем проверить нашу таблицу, чтобы проверить результат:

Лучший способ применения конфигурации и начального числа данных

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

EF Core предоставляет лучший способ создания конфигурации Fluent API с помощью интерфейса IEntityTypeConfiguration<T>. Используя его, мы можем разделить конфигурацию для каждой сущности на отдельный класс конфигурации.

Итак, давайте посмотрим, как это сделать.

В проекте Entities мы собираемся создать новую папку Configuration и внутри нового класса StudentConfiguration:

public class StudentConfiguration : IEntityTypeConfiguration<Student>
{
    public void Configure(EntityTypeBuilder<Student> builder)
    {
        throw new NotImplementedException();
    }
}

Конечно, мы не хотим генерировать исключение (это код по умолчанию после того, как VS реализует интерфейс), поэтому давайте изменим этот метод:

public void Configure(EntityTypeBuilder<Student> builder)
{
    builder.ToTable("Student");
    builder.Property(s => s.Age)
        .IsRequired(false);
    builder.Property(s => s.IsRegularStudent)
        .HasDefaultValue(true);

    builder.HasData
    (
        new Student
        {
            Id = Guid.NewGuid(),
            Name = "John Doe",
            Age = 30
        },
        new Student
        {
            Id = Guid.NewGuid(),
            Name = "Jane Doe",
            Age = 25
        },
        new Student
        {
            Id = Guid.NewGuid(),
            Name = "Mike Miles",
            Age = 28
        }
    );
}

Этот код немного отличается от старого кода OnModelCreating, поскольку нам больше не нужно использовать часть .Entity<Student>. Это потому, что наш объект построителя уже имеет тип EntityTypeBuilder<Student>. Мы добавили дополнительный объект для вставки, просто чтобы было что создать миграцию.

Все, что нам нужно сделать, это изменить метод OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new StudentConfiguration());
}

И все.

Теперь мы можем добавить новую миграцию и применить ее:

PM> Add-Migration AdditionalRowInserted

PM> Update-Database

Настройте начальную миграцию сразу после запуска приложений

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

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

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

Что ж, мы покажем вам, как именно это сделать.

Создание метода расширения

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

public static class MigrationManager
{
}

Теперь нам нужно установить библиотеку Microsoft.ASPNetCore.Hosting.Abstractions (она нужна нам для типа IHost, который мы собираемся использовать в нашем методе расширения) и добавить MigrateDatabase метод расширения для этого класса:

public static class MigrationManager
{
     public static IHost MigrateDatabase(this IHost host)
     {
         using (var scope = host.Services.CreateScope())
         {
             using (var appContext = scope.ServiceProvider.GetRequiredService<ApplicationContext>())
             {
                 try
                 {
                     appContext.Database.Migrate();
                 }
                 catch (Exception ex)
                 {
                     //Log errors or do anything you think it's needed
                     throw;
                 }
             }
         }

         return host;
    }
}

Мы используем тип IHost, потому что это позволяет нам связать этот метод в файле Program.cs и, конечно же, как вы можете видеть, он нам нужен для основной логики. .

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

Применение метода MigrateDatabase

Следующим шагом является вызов этого метода в классе Program.cs:

public static void Main(string[] args)
 {
    CreateWebHostBuilder(args)
        .Build()
        .MigrateDatabase()
        .Run();
 }

Наконец, давайте удалим таблицы Student и _EFMigrationsHistory из базы данных и удалим хранимую процедуру в папке Programmability, чтобы имитировать пустую базу данных (или просто отбросим вашу базу данных: D). Затем мы можем запустить наше приложение. Мы увидим журналы в окне консоли, которые сообщают нам, что миграции выполняются. После того, как миграции завершили свою работу, мы можем проверить базу данных, чтобы убедиться, что все таблицы и процедуры были созданы снова.

Откат и создание сценариев миграции

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

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

Сначала добавим в начальное число еще одну строку:

new Student
{
    Id = Guid.NewGuid(),
    Name = "TEST Name",
    Age = 100
}

Тогда давайте создадим:

PM> Add-Migration RevertTestMigration

и примените миграцию:

PM> Update-Database

В базе данных мы видим, что добавлена новая строка. Но так как мы не удовлетворены этим переносом (гипотетически), давайте вернем его:

PM> Update-Database AdditionalRowInserted

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

Наконец, если мы хотим создать сценарий SQL для всех наших миграций, мы можем сделать это, набрав:

PM> Script-Migration

Эта команда создаст для нас файл сценария.

Заключение

Итак, мы узнали много различной информации о миграции данных и о том, как ее использовать в различных ситуациях в EF Core.

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

В следующей статье мы узнаем больше о конфигурации отношений в ядре EF.