Spis treści
- Wstęp
- Przybornik
- Jak EF migruje bazę danych?
- Uruchomienie migracji
- Podsumowanie
- Zapychacze, czyli pozostałe pliki projektu
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.
- Getting Started with EF Core on ASP.NET Core with a New database
- Migrations - EF Core with ASP.NET Core MVC tutorial (4 of 10)
- Entity Framework Core Migrations
- Enable Entity Framework Core Migrations in Visual Studio 2017
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ą
- Projekt w .NET Core 2.0. Najlepiej ASP.NET Core Web Application
- Baza danych oparta o MS SQL Server
-
Doinstalowanie poniższych nugetów
- Microsoft.AspNetCore.All (ten pewnie już jest)
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer
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
- Stworzyć klasę dziedziczącą z
Migration
- Wskazać, że dotyczy konkretnego kontekstu:
[DbContext(typeof(HelloContext))]
- Nazwać ją, np.
[Migration("201709252218_migracja2")]
. Uwaga na nazwę bo wszystkie nazwy migracji są sortowane alfabetycznie i uruchamiane po kolei
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:
- Stworzenie tabeli z blogami - "201709252151_migracja1" (klasa "hello3")
- Rozszerzenie schematu o posty w blogach - "201709252218_migracja2" (klasa "hello2")
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:
- __EFMigrationsHistory
- Blogs
- Posts
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
- Dodania konfiguracji do SQL Servera
- Uruchomienie migracji za pomocą
DoMigrate.prepareDb(app, logger);
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..."
}
}