Paweł Łukasiewicz
2021-09-05
Paweł Łukasiewicz
2021-09-05
Udostępnij Udostępnij Kontakt
Wprowadzenie

Migracje są sposobem na utrzymanie synchronizacji schematu bazy danych z modelem EF Core. W tym przypadku preferowane jest podejście code-first, które charakteryzuje się większym wsparciem niż podejście odwrotne, tj. database-first. Spójrzcie na poniższy schemat obrazujący to podejście: EFCore: mechanim migracji

Doskonale widać, że API EF Core tworzy model na bazie klasy domenowej (encji). Następny krok to utworzenie lub zaktualizowanie schematu bazy danych na podstawie wygenerowanego modelu EF Core. Za każdym razem gdy zmieniamy klasy domenowe musimy uruchomić migrację celem zachowania aktualności schematu bazy danych.

Migracje to potocznie zestaw poleceń, które można wykonywać w konsoli menadżera pakietów NuGet lub w interfejsie wiersza poleceń dotnet. Spójrzcie poniżej na najpopularniejsze polecenia migracji:

Polecenia konsoli menadżera pakietów NuGet Polecenia dotnet CLI Wykorzystanie
add-migration <nazwa migracji> add <nazwa migracji> Tworzenie migracji poprzez dodanie migawki (określenie różnic).
remove-migration remove Usunięcie ostatniej migracji
update-database update Aktualizacja schematu bazy danych na bazie ostatniej migracji.
script-migration script Utworzenie skryptu SQL zawierającego wszystkie migracje.

Dodawanie migracji

Pierwszym krokiem jest zdefiniowanie początkowych klas domenowych. W tym momencie nie istnieje jeszcze baza danych, która mogłaby przechowywać dane klas domenowych. W tym celu musimy przygotować pierwszą migrację, która będzie w swojej definicji zawierała te zmiany.

Osobiście preferuję użycie NuGet Package Manager: Tools -> NuGet Package Manager -> Package Manager Console. Po otwarciu konsoli wpisujemy poniższe polecenie:

add-migration InitialMigration
Oczywiście alternatywą jest użycie odpowiedniego polecenia dla dotnet CLI:
dotnet ef migrations add InitialMigration
Powyższe polecenie skutkuje dodaniem nowego folderu w strukturze naszego projektu tak jak pokazano poniżej: EFCore: dodawanie migracji Tworząc swoją pierwszą migrację dla nowego projektu dojdzie do utworzenia trzech plików:
  • timestamp_nazwamigracji.cs - główny plik migracji zawierający metody Up() oraz Down(), które modyfikowaliśmy ręcznie w jednym z poprzednich wpisów chcąc dodać kod procedury składowanej. Metoda Up() zawiera kod odpowiedzialny za tworzenie obiektów bazy danych. Metoda Down() jest przeciwieństem – pozwala na usuwanie obiektów z bazy danych;
  • timestamp_nazwamigracji.Designer.cs - plik metadanych migracji zawierający informacje używane przez EF Core;
  • NazwaKontekstuModelSnapshot.cs - migawka bieżącego modelu pozwalająca na określenie zmian podczas tworzenia migracji.
W tym momencie migracja została dodana. Możemy dokonać utworzenia (bądź aktualizacji bieżącego) schematu bazy danych.

Tworzenie lub aktualizacja bazy danych

W tym przypadku wykorzystamy poniższe polecenie korzystając z konsoli pakietów NuGet:

update-database
W przypadku dotnet CLI:
dotnet ef database update

Polecenie Update utworzy bazę danych na podstawie klas kontekstowych oraz migawki migracji, która została utworzona w poprzednim kroku.

Jeżeli jest to nasza pierwsza migracja dojdzie również do utworzenia tabeli __EFMigrationsHistory w której będą przechowywane nazwy wszystkich migracji oraz czas kiedy zostały zastosowane na bieżącej bazie danych. Pamiętajcie, że nazwa bazy danych oraz tabele tworzone są na bazie pliku DbContext oraz konwencji i konfiguracji, które omawialiśmy w poprzednich wpisach. W celu przygotowania tego wpisu wykorzystałem jeden z poprzednich projektów:

public class ApplicationDbContext : DbContext
{
	// Nazwa pierwszej wygenerowanej tabeli
	public DbSet<CarCompanies> CarCompanies { get; set; }

	// Nazwa drugiej wygenerowanej tabeli
	public DbSet<CarComapniesData> CarCompaniesData { get; set; }

	public ApplicationDbContext()
	{

	}

	// W naszej prostej aplikacji konsolowej będziemy się trzymać konwencji
	// ze zdefiniowaniem connection-string w metodzie OnConfiguring
	protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
	{
		if (!optionsBuilder.IsConfigured)
		{
			// Connection String: database określa nazwę bazy do której się łączymy: EFCoreCodeFirst
			optionsBuilder.UseSqlServer(@"Server=PAWEL;database=EFCoreCodeFirst;Integrated Security=true");
		}
	}
}
Tak wygląda baza danych po wywołaniu powyższego polecenia: EFCore: pierwsza migracja

Usuwanie migracji

Ostatnia migracja może zostać usunięta o ile nie została nałożona na bazę danych. W tym celu możemy wykorzystać polecenie do usuwania ostatniej migracji:

// Package Manager Console
remove-migration

// dotnet CLI
dotnet ef migration remove

Powyższe polecenia usuwają ostatnią migrację. Musimy jednak pamiętać, że po wykonaniu polecenia update-database i próbie usunięcia migracji zobaczymy poniższy wyjątek:

The migration <migration name> has already been applied to the database. Revert it and try again. If the migration has been applied to other databases, consider reverting its changes using a new migration.
Jak doskonale widzicie w pierwszej kolejności musimy dokonać cofnięcia ostatniej migracji.

Cofanie migracji

Jest to niezwykle ważny krok dla osób zaczynających swoją przygodę z Entity Framework Core. Moje pierwszy próby kończyły się zwykle na całkowitym usunięciu bazy danych, ręcznym usunięciu migracji i ponownych próbach. Tak, na początkowym etapie nic wielkiego się nie stanie ale wraz z rozwojem naszego projektu taki krok jest niedopuszczalny ponieważ może wyrządzić więcej szkód niż pożytku.

Przeanalizujmy sytację: pierwsza migracja została nałożona na bazę danych. Pracujemy dalej nad rozwojem projektu, dokonujemy zmian w klasach domenowych, tworzymy kolejną migrację i dokonujemy aktualizacji schematu bazy danych. Okazuje się jednak, że nasze zmiany są złe a my chcemy przywrócić bazę danych do poprzedniego stanu. W takiej sytuacji używamy polecenia:

// Package Manager Console
update-database <nazwa migracji>

// dotnet CLI
dotnet ef database update <nazwa migracji>
w celu przywrócenia schematu bazy danych do określonej migracji.

Powyższe polecenie przywróci bazę danych do migracji o nazwie InitialMigration (po wskazaniu takiej nazwy migracji) oraz usunie wszelkie zmiany wynikające z zastosowania kolejnej migracji. Dodatkowo, dojdzie do usunięcia informacji o drugiej migracji z tabeli __EFMigrationHistory. Krok ten nie spowoduje jednak usunięcia plików związanych ze wskazaną migracją. Tym razem możemy jednak wykorzystać polecenie remove-migration w celu uporządkowania plików w folderze naszej migracji.

Generowanie skryptu SQL

Wraz z dodawaniem kolejnych migracji należy je wdrożyć i zastosować na odpowiednich bazach danych. Musimy pamietać, że istnieją różne strategie pozwalające na zrealizowanie tego kroku. Inaczej pracujemy w środowisku deweloperskim a inne kroki zostaną zastosowane przy wdrażaniu zmian na środowisku produkcyjnym.

Tworzenie skryptu SQL o którym tutaj mowa pozwoli na wygenerowanie jednego skryptu dla wszystkich migracji. Możemy również wskazać zakres migracji, które mają być składowymi skryptu wykorzystując operatory -from oraz -to. Generowanie skryptu odbywa się przy pomocy poniższych poleceń:

// Package Manager Console
script-migration

// dotnet CLI
dotnet ef migrations script

Wynikiem działania powyższego polecenia jest utworzenie pliku *.sql, który dla naszego prostego przykładu tworzy 2 puste oraz 1 tabelę z informacjami dotyczącymi wdrożonych migracji:

IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL
BEGIN
    CREATE TABLE [__EFMigrationsHistory] (
        [MigrationId] nvarchar(150) NOT NULL,
        [ProductVersion] nvarchar(32) NOT NULL,
        CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
    );
END;
GO

BEGIN TRANSACTION;
GO

CREATE TABLE [CarCompanies] (
    [CompanyId] int NOT NULL IDENTITY,
    [Brand] nvarchar(max) NULL,
    CONSTRAINT [PK_CarCompanies] PRIMARY KEY ([CompanyId])
);
GO

CREATE TABLE [CarCompaniesData] (
    [Id] int NOT NULL IDENTITY,
    [Sale] int NOT NULL,
    [CompanyId] int NOT NULL,
    [CarCompaniesCompanyId] int NULL,
    CONSTRAINT [PK_CarCompaniesData] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_CarCompaniesData_CarCompanies_CarCompaniesCompanyId] FOREIGN KEY ([CarCompaniesCompanyId]) REFERENCES [CarCompanies] ([CompanyId]) ON DELETE NO ACTION
);
GO

CREATE INDEX [IX_CarCompaniesData_CarCompaniesCompanyId] ON [CarCompaniesData] ([CarCompaniesCompanyId]);
GO

INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20210108100524_InitialMigration', N'5.0.0');
GO

COMMIT;
GO