Управление схемой БД и миграции

Данное руководство устарело. Актуальное руководство: Руководство по Entity Framework Core 7

Последнее обновление: 15.11.2020

Если мы меняем модели в Entity Framework, которые входят в контекст данных, например, добавляем в нее какие-то новые свойства или удаляем некоторые свойства, то необходимо, чтобы база данных также применяла эти изменения. Например, в прошлых темах был создан класс User, который описывал пользователя:

public class User
{
	public int Id { get; set; }
	public string Name { get; set; }
	public int Age { get; set; }
}

А для работы с базой данных использовался следующий контекст данных:

public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public ApplicationContext()
    {
        Database.EnsureCreated();
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=helloappdb;Trusted_Connection=True;");
    }
     
}

Допустим, мы хотим добавить в класс User новое свойство, например:

public class User
{
	public int Id { get; set; }
	public string Name { get; set; }
	public int Age { get; set; }
	public string Position { get; set; }	// Новое свойство - должность пользователя
}

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

Изменение модели в Entity Framework Core

Так как модель User изменилась, то нам надо привести в соответствие соответствующую таблицу в БД. В зависимости от конкретной ситуации можно использовать ряд подходов для этого.

Ручное изменение базы данных

В самых простых случаях мы можем написать sql-скрипт для добавления столбцов или таблиц, либо же даже можем вручную в режиме дизайнера таблицы в Visual Studio добавить столбцы и/или таблицы. Для этого перейдем к дизайнеру таблицы:

Ручное изменение таблицы в Entity Framework Core Изменение таблицы в Entity Framework Core

Например, свойству Position с типом string будет соответствовать столбец Position с типом NVARCHAR. После добавления столбца нажмем на кнопку Update и далее на кнопку Update Database:

В итоге будет добавлен новый столбец. И теперь таблица Users находится в соответствии с классом User. Больше никаких проблем при выполнении программы не возникнет.

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

Тем не менее этот подход имеет много недостатков. В частности, менее искушенные программисты могут не знать, как сопоставляются типы между SQL и C#. При указании данных столбцов и/или таблиц мы можем допустить ошибку - например, вместо "Position" написать "Positon". В конце концов такой подход может занять много времени, особенно когда речь идет о куда больших изменениях схемы БД.

Database.EnsureCreated и Database.EnsureDeleted

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

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

public class ApplicationContext : DbContext
{
	public DbSet<User> Users { get; set; }
	public ApplicationContext()
	{
		Database.EnsureDeleted();	// удаляем бд со старой схемой
		Database.EnsureCreated();	// создаем бд с новой схемой
	}
	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
	{
		optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=helloappdb;Trusted_Connection=True;");
	}
}

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

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

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

Миграция

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

Для создания миграции в окне Package Manager Console вводится следующая команда:

Add-Migration название_миграции

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

После создания миграции ее надо выполнить с помощью команды:

Update-Database

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

Для использования миграций в Visual Stuido необходимо добавить в проект через менеджер Nuget пакет Microsoft.EntityFrameworkCore.Tools.

Например, определим модели и контекст следующим образом:

public class User
{
	public int Id { get; set; }
	public string Name { get; set; }
	public int Age { get; set; }
}
public class ApplicationContext : DbContext
{
	public DbSet<User> Users { get; set; }
	public ApplicationContext()
	{
	//    Database.EnsureCreated();
	}
	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
	{
		optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=userappdb;Trusted_Connection=True;");
	}
}

Обратите внимание, что в конструкторе контекста закомментирован метод Database.EnsureCreated(). В данном случае он не нужен. Более того при выполнении миграции этот метод вызывает ошибку. Этот момент следует учитывать.

Теперь для создания и выполнения миграции перейдем в Visual Studio к окну Package Manager Console. Вначале введем команду

Add-Migration InitialCreate

Название миграции произвольное. В данном случае это InitialCreate. Нажмем на Enter для создания миграции.

После этого в проект будет добавлена папка Migrations с классом миграции:

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

Папка содержит три файла:

  • XXXXXXXXXXXXXX_InitialCreate.cs: основной файл миграции, который содержит все применяемые действия

  • XXXXXXXXXXXXXX_InitialCreate.Designer.cs: файл метаданных миграции, которые используются Entity Frameworkом

  • [Имя_контекста_данных]ModelSnapshot.cs: содержит текущее состояние модели, используется при создании следующей миграции

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

using Microsoft.EntityFrameworkCore.Migrations;

namespace HelloApp.Migrations
{
    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Users",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Age = table.Column<int>(type: "int", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Users", x => x.Id);
                });
        }

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

В миграции определяются два метода: Up() и Down(). В методе Up с помощью вызова метода CreateTable добавляется новое определение таблиц.

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

Update-Database
Выполнение миграции в Entity Framework Core

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

Если мы изменим модель, например, добавим в класс User новое свойство:

public class User
{
	public int Id { get; set; }
	public string Name { get; set; }
	public int Age { get; set; }
	public bool IsMarried { get; set; }
}

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

Add-Migration IsMarriedToUserAdded
Update-Database

В данном случае будет создан класс миграции, который отражает добавление нового свойства в класс User:

using Microsoft.EntityFrameworkCore.Migrations;

namespace HelloApp.Migrations
{
    public partial class IsMarriedToUserAdded : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AddColumn<bool>(
                name: "IsMarried",
                table: "Users",
                type: "bit",
                nullable: false,
                defaultValue: false);
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropColumn(
                name: "IsMarried",
                table: "Users");
        }
    }
}

Метод AddColumn как раз добавляет новый столбец в таблицу.

Миграции в консоли

Если разработка осуществляется не в Visual Studio, то для миграций мы можем выполнять соответствующие команды в консоли. Для создания миграции:

dotnet ef migrations add InitialCreate

Для выполнения миграции:

dotnet ef database update

Метод Migrate

В некоторых случаях, например, в приложениях с локальной базой данных (SQLite в UWP), мы можем выполнять миграции в процессе выполнения приложения. Для этого определен метод Database.Migrate(), который можно вызвать через объект контекста:

myDbContext.Database.Migrate();

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

Чтобы задействовать этот метод, необходимо подключить пространство имен Microsoft.EntityFrameworkCore

Миграция, если конструктор контекста принимает параметр DbContextOptions

Выше была рассмотрена миграция для контекста данных, который имеет конструктор без параметров и устанавливает настройки подключения в методе OnConfiguring(). Однако мы можем также передавать параметры подключения в контекст данных извне через конструктор с параметром типа DbContextOptions:

public class ApplicationContext : DbContext
{
	public DbSet<User> Users { get; set; }
	public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options)
	{
	}
}

Например, у нас в проекте есть файл конфигурации appsettings.json:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\mssqllocaldb;Database=helloappdb32;Trusted_Connection=True;"
  }
}

Из которого в процессе выполнения приложения мы извлекаем строку подключения и передаем в контекст данных:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Linq;

namespace HelloApp
{
    class Program
    {
        static void Main(string[] args)
        {

            var builder = new ConfigurationBuilder();
            // установка пути к текущему каталогу
            builder.SetBasePath(Directory.GetCurrentDirectory());
            // получаем конфигурацию из файла appsettings.json
            builder.AddJsonFile("appsettings.json");
            // создаем конфигурацию
            var config = builder.Build();
            // получаем строку подключения
            string connectionString = config.GetConnectionString("DefaultConnection");

            var optionsBuilder = new DbContextOptionsBuilder<ApplicationContext>();
            var options = optionsBuilder
                .UseSqlServer(connectionString)
                .Options;

            using (ApplicationContext db = new ApplicationContext(options))
            {
                var users = db.Users.ToList();
                foreach (User u in users)
                {
                    Console.WriteLine(u.Name);
                }
            }
        }
    }
}

Как получать конфигурацию подключения из файла, описывалось в статье Конфигурация подключения

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

PM> Add-Migration InitialCreate
Build started...
Build succeeded.
Unable to create an object of type 'ApplicationContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728

Дело в том, что, если единственный конструктор класса контекста принимает параметр DbContext:

public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options){ }

В этом случае при выполнении миграции инструментарий Entity Frameworkа ищет класс, который реализует интерфейс IDesignTimeDbContextFactory и который задает конфигурацию контекста.

Поэтому в этом случае нам необходимо добавить в проект подобный класс. Например:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;

namespace HelloApp
{
    public class SampleContextFactory : IDesignTimeDbContextFactory<ApplicationContext>
    {
        public ApplicationContext CreateDbContext(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<ApplicationContext>();
			
			// получаем конфигурацию из файла appsettings.json
            ConfigurationBuilder builder = new ConfigurationBuilder();
            builder.SetBasePath(Directory.GetCurrentDirectory());
            builder.AddJsonFile("appsettings.json");
            IConfigurationRoot config = builder.Build();

            // получаем строку подключения из файла appsettings.json
            string connectionString = config.GetConnectionString("DefaultConnection");
            optionsBuilder.UseSqlServer(connectionString, opts => opts.CommandTimeout((int)TimeSpan.FromMinutes(10).TotalSeconds));
            return new ApplicationContext(optionsBuilder.Options);
        }
    }
}

Класс SampleContextFactory применяет интерфейс IDesignTimeDbContextFactory, который типизируется типом контекста данных - в данном случае класс ApplicationContext. Данный интерфейс содержит один метод CreateDbContext(), который должен возвращать созданный объект контекста данных.

В данном случае также получаем конфигурацию из файла appsettings.json и извлекаем из ее строку подключения и таким образом создаем контекст.

Хотя этот класс формально нигде не вызывается и никак не используется, фактически он вызывается инфраструктурой Entity Framework при создании миграции.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850