Spis treści

  1. Wstęp
  2. Przybornik
  3. Jak EF migruje bazę danych?
  4. Uruchomienie migracji
  5. Podsumowanie
  6. Zapychacze, czyli pozostałe pliki projektu
    1. Plik Startup.cs
    2. Modele i kontekst
    3. Plik appsettings.json

Wstęp

Migracja bazy danych a w zasadzie jej stworzenie przy podejściu CodeFirst dość dobrze jest opisana w artykułach od Microsoftu (i nie tylko) do których linki są poniżej.

Co jeśli jednak to za dużo magii i chce się mniej więcej poznać jak EntityFramework (EF) wykonuje te migracje? Jak taką migrację przygotować ręcznie?

Przybornik

Do tego zadania potrzebne będą

Jak EF migruje bazę danych?

Wykonanie migracji bazy danych w EF jest niezwykle skomplikowane i do jej wykonania potrzebna jest aż jedna linia:

context.Database.Migrate();

Prosto jest uruchomić migrację ale żeby przygotować ręcznie skrypt migracyjny bez używania narzędzi należy

Plik "HelloMigrations.cs" specjalnie zawiera klasy, których nazwy są w odwrotnej kolejności alfabetycznej niż te umieszczone w atrybutach Migration aby pokazać, że się nie liczą.

Migracja powinna zostać wykonana w kolejności:

using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace aspCoreMigr
{
    [DbContext(typeof(HelloContext))]
    [Migration("201709252218_migracja2")]
    public class hello2 : Migration
    {
        protected override void Up(MigrationBuilder builder)
        {
            builder.CreateTable(
                name: "Posts",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    BlogId = table.Column<int>(nullable: false),
                    Name = table.Column<string>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Posts_Id", k => k.Id);
                    table.ForeignKey("FK_Posts_Blogs", p => p.BlogId, "Blogs", nameof(Blog.Id));
                }
                );
        }
    }

    [DbContext(typeof(HelloContext))]
    [Migration("201709252151_migracja1")]
    public class hello3 : Migration
    {
        protected override void Up(MigrationBuilder builder)
        {
            //builder.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

            builder.CreateTable(
                name: "Blogs",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    Url = table.Column<string>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Blogs_Id", k => k.Id);
                }
                );
        }
    }
}

EF przegląda bibliotekę w poszukiwaniu klas spełniających wskazane wyżej kryteria i uruchamia migrację, którą widać w logach konsoli

info: Microsoft.EntityFrameworkCore.Database.Command[200101]
      Executed DbCommand (127ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [MigrationId], [ProductVersion]
      FROM [__EFMigrationsHistory]
      ORDER BY [MigrationId];
info: Microsoft.EntityFrameworkCore.Migrations[200402]
      Applying migration '201709252151_migracja1'.
info: Microsoft.EntityFrameworkCore.Database.Command[200101]
      Executed DbCommand (173ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [Blogs] (
          [Id] int NOT NULL IDENTITY,
          [Url] nvarchar(max) NOT NULL,
          CONSTRAINT [PK_Blogs_Id] PRIMARY KEY ([Id])
      );
info: Microsoft.EntityFrameworkCore.Database.Command[200101]
      Executed DbCommand (131ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
      VALUES (N'201709252151_migracja1', N'2.0.0-rtm-26452');
info: Microsoft.EntityFrameworkCore.Migrations[200402]
      Applying migration '201709252218_migracja2'.
info: Microsoft.EntityFrameworkCore.Database.Command[200101]
      Executed DbCommand (139ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [Posts] (
          [Id] int NOT NULL IDENTITY,
          [BlogId] int NOT NULL,
          [Name] nvarchar(max) NOT NULL,
          CONSTRAINT [PK_Posts_Id] PRIMARY KEY ([Id]),
          CONSTRAINT [FK_Posts_Blogs] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id])
      );
info: Microsoft.EntityFrameworkCore.Database.Command[200101]
      Executed DbCommand (130ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
      VALUES (N'201709252218_migracja2', N'2.0.0-rtm-26452');

Po takiej operacji w bazie znajdują się trzy dodatkowe tabele:

Kolejne uruchomienie migracji nie spowoduje żadnego efektu ponieważ EF zapisuje wykonane migracje w tabeli [__EFMigrationsHistory]

info: Microsoft.EntityFrameworkCore.Migrations[200405]
      No migrations were applied. The database is already up to date.

Uruchomienie migracji

O ile wywołanie procesu migracji to jedna linia to jednak trochę kodu trzeba przygotować aby tą linię wykonać - plik "DbMigrate.cs"

public static class DoMigrate
{
    public static void prepareDb(IApplicationBuilder app, ILogger<Startup> logger)
    {
        using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<HelloContext>();

            // wykonanie migracji
            context.Database.Migrate();

            // sprawdzenie poprawności - odczyt i/lub zapis

            const string bloUrl = "http://pieszynski.com/";

            Blog dotnetowy = context.Blogs
                .Include(i => i.BlogPosts)
                .FirstOrDefault(f => f.Url == bloUrl);

            if (null == dotnetowy)
            {
                dotnetowy = new Blog
                {
                    Url = bloUrl,
                    BlogPosts = new List<Post>
                    {
                        new Post
                        {
                            Name = "Pierwszy!!111"
                        }
                    }
                };
                context.Blogs.Add(dotnetowy);
                context.SaveChanges();

                logger.LogWarning(
                    10,
                    $"Nowy blog: {dotnetowy.Id} i post {dotnetowy.BlogPosts[0].Id}"
                    );
            }
            else
                logger.LogWarning(
                    20,
                    $"Blog juz istnial pod: {dotnetowy.Id}" +
                    $" i ma {dotnetowy.BlogPosts?.Count ?? -1} postow"
                    );
        }
    }
}

Podsumowanie

Skoro już wiadomo jak działa migracja bazy za pomocą EF to lepiej ją tworzyć nie ręcznie lecz przez narzędzia takie jak np. PS> Add-Migration nazwa_migracji, ponieważ dodają one dodatki związane ściśle z technologią danego serwera bazodanowego (np. "SqlServer:ValueGenerationStrategy").

Warto jednak, jeśli zachodzi taka potrzeba do migracji dopisywać własne zapytania związane z rzeczami, o których EF nie może mieć pojęcia - builder.Sql(...)

Zapychacze, czyli pozostałe pliki projektu

Plik Startup.cs

Zawartość jest praktycznie szablonowa za wyjątkiem

Modele i kontekst

[Table("Blogs")]
public class Blog
{
    [Key]
    public int Id { get; set; }
    public string Url { get; set; }
    
    public List<Post> BlogPosts { get; set; }
}

[Table("Posts")]
public class Post
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
}

public class HelloContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    public HelloContext(DbContextOptions options) : base(options) { }
}
public class Startup
{
    IConfigurationRoot Configuration;

    public Startup(IHostingEnvironment env)
    {
        Configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json")
            .Build();
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<HelloContext>(builder =>
        {
            builder
                .UseSqlServer(Configuration.GetConnectionString("ContextConnection"))
                .EnableSensitiveDataLogging();
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILogger<Startup> logger)
    {
        DoMigrate.prepareDb(app, logger);

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    }
}

Plik appsettings.json

{
  "ConnectionStrings": {
    "ContextConnection": "Server=itp, itd..."
  }
}