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

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

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

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

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

using Microsoft.EntityFrameworkCore;

public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;
    public ApplicationContext()
    {
        Database.EnsureCreated();
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=helloapp.db");
    }
}

Допустим, мы хотим добавить в класс 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, например, получить данные этой модели

using (ApplicationContext db = new ApplicationContext())
{
    var users = db.Users.ToList();
    Console.WriteLine("Список пользователей:");
    foreach (User u in users)
    {
        Console.WriteLine($"{u.Id}.{u.Name} - {u.Age}");
    }
}

то мы столкнемся с ошибкой.

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

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

В зависимости от конкретной ситуации можно использовать ряд подходов для этого. Рассмотрим их.

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

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

Например, в примере выше применялась база данных SQLite. Для ее редактирования мы можем использовать программу DB Browser for SQLite. Так, откроем базу данных в этой программе. Нажмем на таблицу Users правой кнопкой мыши и в появившемся контекстном меню выберем Modify Table:

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

В окне редактирования таблицы нажмем на кнопку Add для добавления нового столбца. После в определении таблицы добавится новая строка для определения нового столбца, где для названия столбца введем "Position", а в качестве типа столбца опеделим TEXT

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

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

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

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

Database.EnsureCreated и Database.EnsureDeleted

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

using Microsoft.EntityFrameworkCore;

public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;
    public ApplicationContext()
    {
        Database.EnsureDeleted();   // удаляем бд со старой схемой
        Database.EnsureCreated();   // создаем бд с новой схемой
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=helloapp.db");
    }
}

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

Миграция

Миграция по сути предствляет план перехода базы данных от старой схемы к новой. Как использовать миграции? Прежде всего необходимо добавить в проект пакет Microsoft.EntityFrameworkCore.Tools. Кроме того, если мы работаем через .NET CLI, то также надо установить инструменты для EF Core с помощью команды:

dotnet tool install --global dotnet-ef

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

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

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

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

Update-Database

При работе с .NET CLI для создания миграции вводится команда:

dotnet ef migrations add InitialCreate

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

dotnet ef database update

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

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

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; } = null!;
	public ApplicationContext()
	{
	//    Database.EnsureCreated();
	}
	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
	{
		optionsBuilder.UseSqlite("Data Source=D:\\helloapp.db");
	}
}

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

Также стоит отметить, что при самом первом применении миграции по отношению к БД SQLite Entity Framework пытается создать ее заново, однако если создаваемые таблицы в ней уже есть, то мы столкнемся с ошибкой. Поэтому следует убедиться, что по используемому пути нет файла базы данных с подобным именем. При последующих применениях миграции EF будет использовать бд, созданную при первой миграции.

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

Add-Migration InitialCreate

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

Если работаем в .NET CLI, то выполняем следующую команду:

dotnet ef migrations add InitialCreate

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

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

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

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

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

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

using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

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: "INTEGER", nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    Name = table.Column<string>(type: "TEXT", nullable: true),
                    Age = table.Column<int>(type: "INTEGER", 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 добавляется новое определение таблиц.

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

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

При работе с .NET CLI выполняем следующую команду:

dotnet ef database update

После выполнения миграции по указанному в методе optionsBuilder.UseSqlite("Data Source=D:\\helloapp.db") пути будет сгенерированная база данных. В случае с бд SQLite, для которой указан относительный путь (например, "Data Source=helloapp.db"), файл бд генерируется в папке проекта. Для других провайдеров - MS SQL Server, MySQL и т.д. бд генерируется на сервере бд в соответствии со строкой подключения.

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

Выполнение миграции базы данных Sqlite и _EFMigrationsHystory в Entity Framework Core

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

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

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

Add-Migration IsMarriedToUserAdded
Update-Database

Создание и выполнение в .NET CLI:

dotnet ef migrations add IsMarriedToUserAdded
dotnet ef database update

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

using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

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

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

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

Метод Migrate

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

using Microsoft.EntityFrameworkCore;

using (ApplicationContext db = new ApplicationContext())
{
    db.Database.Migrate();	// миграция
	await db.Database.MigrateAsync(); // асинхронный метод для миграции

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

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

Создания скрипта sql для миграции

Entity Framework также позволяет создать из файлов миграции скрипт sql, который потом можно запустить для создания или реорганизации базы данных. Для создания скрипта sql в Visual Studio необходимо ввести в окне Package Manager Console команду

Script-Migration

Для создания скрипта sql по миграции в .NET CLI применяется следующая команда:

dotnet ef migrations script

В результате выполнения этой команды будет создан и открыт скрипт sql. Например, по выше созданной миграции InitialCreate будет создан следующий скрипт:

CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
    "MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
    "ProductVersion" TEXT NOT NULL
);

BEGIN TRANSACTION;

CREATE TABLE "Users" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT,
    "Name" TEXT NULL,
    "Age" INTEGER NOT NULL,
    "IsMarried" INTEGER NOT NULL
);

INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20211117182828_InitialCreate', '6.0.0');

COMMIT;

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

Script-Migration InitialCreate

Для .NET CLI:

dotnet ef migrations script InitialCreate

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

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

using Microsoft.EntityFrameworkCore;
 
public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;
    public ApplicationContext(DbContextOptions<ApplicationContext> options)
            : base(options) { }
}

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

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=D:\helloapp2.db"
  }
}

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

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

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

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

using (ApplicationContext db = new ApplicationContext(options))
{
    User tom = new User { Name = "Tom", Age = 33 };
    db.Users.Add(tom);
    db.SaveChanges();

    var users = db.Users.ToList();
    foreach (User u in users)
    {
        Console.WriteLine($"{u.Id}.{u.Name} - {u.Age}");
    }
}

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

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

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;

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.UseSqlite(connectionString);
        return new ApplicationContext(optionsBuilder.Options);
    }
}

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

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

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

Объединение миграций

Начиная с версии 6.0 Entity Framework позволяет создавать бандлы миграций - объединение миграций в виде исполняемого файла. Для создания бандла миграций надо в Visual Studio в окне Package Manager Console выполнить команду:

Bundle-Migration

А если использовуется .NET CLI, то в консоли надо перейти к папке проекта и выполнить команду

dotnet ef migrations bundle

После выполнения этих команд в папке решения будет сгенерирован файл efbundle (в Windows он будет иметь расширение exe). Запустим его.

Объединение миграций в Entity Framework Core и C#

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

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