Migracja schematu bazy danych w Entity Framework

Wprowadzenie

W obecnych czasach bardzo rzadko tworzy się aplikacje, które nie korzystają z bazy danych. Gdzieś przecież trzeba zapisać dane. Od jakiegoś czasu coraz bardziej popularne stają są bazy nierelacyjne, ale nadal najczęściej używamy baz relacyjnych, takich jak na przykład SQL Server. Jednym z problemów, z jakimi musimy się zmierzyć, jest zmiana schematu bazy danych na przestrzeni czasu. A to musimy dodać miejsce na nowe dane, usunąć już istniejące czy też zmienić strukturę, aby zapytania wykonywały się szybciej.

Pracując z różnymi firmami, zespołami czy to pojedynczymi developerami, zauważyłem, że jest to jeden z problemów, których najczęściej programiści nie umieją dobrze rozwiązać. A to w repozytorium znajdują się dziesiątki skryptów sql, które tworzą schemat bazy, i tylko niektóre osoby wiedzą, w jaki sposób je wykonać, aby baza działała poprawnie. Tu znów każda wdrożona baza ma nieco inną strukturę, gdzie później doprowadzenie wszystkiego do ładu zajmuje wiele cennych godzin. A to… Zapewne sam masz w pamięci jakąś nieciekawą sytuację, a może nawet na co dzień musisz z tym walczyć.

Dlatego właśnie postanowiłem przygotować trzy wpisy na temat różnych bibliotek, które możesz wykorzystać do zarządzania schematem bazy danych, aby ta kwestia przestała być problemem. Dzisiaj na pierwszy rzut pójdą migracje w Entity Framework. W kolejnych dwóch wpisach pokażę Ci inne biblioteki, które rozwiązują ten problem w trochę inny sposób. Dzięki temu będziesz mógł/mogła wybrać to, co najlepiej odpowie Twoim potrzebom.

Migracje w Entity Framework

Entity Framework od dłuższego czasu wspiera migracje schematu bazy danych w bardzo przyjemny sposób. Migracje są generowane w sposób automatyczny lub ręczny na podstawie zmian w modelu obiektowym aplikacji. Dzięki temu nie musimy w większości przypadków w ogóle dotykać sqla, ale, jak zobaczysz później, nie zawsze możemy z niego zrezygnować.

Najfajniejsze jest to, że Entity Framework pilnuje uruchamiania migracji na bazie. W specjalnej tabeli zapisuje informacje dotyczące tego, które migracje się wykonały – i nie musimy ręcznie tego pilnować.

Na ogół tworzymy dedykowanych projekt w solution, w którym wrzucamy cały kod związany z warstwą dostępu do danych. W tym projekcie znajduje się obiekt kontekstowy z Entity Framework oraz – później, z czasem – migracje. Aby włączyć mechanizm migracji w tym projekcie wykonujemy komendę „enable-migrations” w konsoli Nugeta (wybieramy projekt warstwy dostępu do danych w konsoli, aby nie trzeba było tego określać w wywołaniu komendy):

entity framework enable migrations

Po wykonaniu tej komendy w projekcie pojawi się nowy katalog (Migrations), w którym na początku znajduje się jedna klasa (Configuration), a z czasem będą pojawiać się nowe klasy z migracjami (poniższy zrzut ekranu pokazuje testowy projekt z dodanymi migracjami):

entity framework solution with migrations

Tutaj jeszcze mała uwaga: komendy Entity Framework wykonywane w konsoli Nugeta są wykonywane na aktualnie wybranym projekcie. Dodatkowo część komend potrzebuje connection stringa do bazy. Wartość jest brana z konfiguracji projektu, który jest ustawiony jako projekt startowy (pogrubiona nazwa w Solution Explorer – w powyższym rzucie ekranu jest to EFMigrationExample.Migrator). Warto o tym pamiętać w momencie, gdy dostaniesz błąd o tym, że nie udało się znaleźć connection stringa.

Dodanie migracji

Dodanie nowej migracji w Entity Framework nie jest trudne. Wystarczy na początku dodać nową klasę modelu do aplikacji lub zmienić już istniejącą. W przykładzie w pierwszej kolejności dodałem klasę Product, która wygląda tak:

public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
}
view raw Product.cs hosted with ❤ by GitHub

Klasa składa się z trzech prostych właściwości: ID dla klucza głównego w tabeli, Name – nazwy produktu, Category – nazwy kategorii. Dodanie nowej migracji sprowadza się do wywołania w konsoli Nugeta komendy „add-migration AddProduct”, gdzie AddProduct to nazwa migracji. Ostateczna migracja wygląda tak:

public partial class AddProduct : DbMigration
{
public override void Up()
{
CreateTable(
"dbo.Products",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
Category = c.String(),
})
.PrimaryKey(t => t.Id);
Sql("INSERT INTO dbo.Products VALUES ('Product 1.1', 'Category 1');");
Sql("INSERT INTO dbo.Products VALUES ('Product 1.2', 'Category 1');");
Sql("INSERT INTO dbo.Products VALUES ('Product 1.3', 'Category 1');");
Sql("INSERT INTO dbo.Products VALUES ('Product 2.1', 'Category 2');");
Sql("INSERT INTO dbo.Products VALUES ('Product 2.2', 'Category 2');");
}
public override void Down()
{
DropTable("dbo.Products");
}
}
view raw AddProduct.cs hosted with ❤ by GitHub

Jak widać, w migracji mamy dwie metody. Up podnosi wersję bazy danych. Down cofa zmiany. Większość kodu została wygenerowana przez Entity Framework. Sam ręcznie dodałem Inserty, aby mieć jakieś dane testowe, które posłużą mi w testowaniu kolejnej migracji.

Aby wykonać migrację, możemy używać komendy „update-database” w konsoli Nugeta. Warto dodać jeszcze przełącznik „-verbose”, który spowoduje, że w konsoli wyświetli się dużo więcej informacji podczas wykonywania aktualizacji bazy danych. Między innymi zostaną wyświetlone wszystkie wykonane sqle. Później jeszcze wrócimy do tego, jak można wykonywać migracje w inny sposób.

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 30 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Dodanie kolejnej migracji

Aby zobaczyć migracje w praktyce, warto zmienić nieco model danych i wygenerować kolejną migrację. Na potrzeby przykładu załóżmy, że chcemy zapisywać inaczej informacje o kategorii produktu. Zamiast właściwości typu string w produkcie chcemy teraz przechowywać te informacje w dedykowanej tabeli. Po zmianie model danych wygląda tak:

public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
}
view raw Product2.cs hosted with ❤ by GitHub
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
view raw Category.cs hosted with ❤ by GitHub

Możemy teraz wygenerować kolejną migrację „add-migration AddCategory.cs”, która wygląda tak:

public partial class AddCategory : DbMigration
{
public override void Up()
{
CreateTable(
"dbo.Categories",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
})
.PrimaryKey(t => t.Id);
AddColumn("dbo.Products", "CategoryId", c => c.Int(nullable: false));
Sql("INSERT INTO dbo.Categories SELECT DISTINCT Category FROM dbo.Products;");
Sql("UPDATE p SET p.CategoryId = (SELECT c.Id FROM dbo.Categories c WHERE c.Name = p.Category) FROM dbo.Products p;");
CreateIndex("dbo.Products", "CategoryId");
AddForeignKey("dbo.Products", "CategoryId", "dbo.Categories", "Id", cascadeDelete: true);
DropColumn("dbo.Products", "Category");
}
public override void Down()
{
AddColumn("dbo.Products", "Category", c => c.String());
Sql("UPDATE p SET p.Category = (SELECT c.Name FROM dbo.Categories c WHERE c.Id = p.CategoryId) FROM dbo.Products p;");
DropForeignKey("dbo.Products", "CategoryId", "dbo.Categories");
DropIndex("dbo.Products", new[] { "CategoryId" });
DropColumn("dbo.Products", "CategoryId");
DropTable("dbo.Categories");
}
}
view raw AddCategory.cs hosted with ❤ by GitHub

Podobnie jak w poprzedniej migracji, większość kodu została wygenerowana przez Entity Framework. Sam ręcznie dodałem dwa sqle w metodzie Up oraz jeden w metodzie Down. Dodatkowe sqle są odpowiedzialne za przeniesienie danych z kolumny Category do nowej tabeli oraz ustawienie odpowiednich CategoryId. W przypadku metody Down po prostu kopiuję z powrotem dane z tabeli do kolumny.

Tym przykładem chciałem pokazać, że bardzo często podczas zmiany schematu bazy danych musimy również zadbać o zmianę danych. Tak, jak to jest w przykładzie, druga migracja kopiuje dane z jednej tabeli do drugiej, abyśmy w aplikacji nie stracili danych.

Ten przykład pokazuje również, że automatyczne migracje dostępne w Entity Framework na dłuższą metę są problematyczne. Dlatego za każdym razem zalecam, aby z nich w ogóle nie korzystać. Zacznij od razu generować migracje w formie kodu – wtedy masz dużo większą elastyczność i możesz wykonywać dodatkowe rzeczy w samym sqlu.

Aby przetestować migrację, wykonujemy ponownie „update-database” w konsoli Nugeta.

Uruchamianie migracji

Migracje możemy wykonywać w różny sposób na bazie danych. Pokazany wyżej sposób wykorzystujący konsolę Nugeta jest dobry podczas programowania. Jednak już w przypadku środowisk testowych i produkcyjnych jest problematyczny.

Jednym z sposób jest skonfigurowanie Entity Framework w ten sposób, aby automatycznie wykonywał migrację podczas pierwszego zapytania w aplikacji. Entity Framework za każdym razem na początku sprawdza aktualny schemat bazy (informacje w tabeli __MigrationHistory, a nie fizyczny schemat bazy) i porównuje go z tym, co ma w modelu w aplikacji. Gdy schemat się różni, może automatycznie wykonać brakujące migracje. Aby to włączyć, wystarczy w web.config lub app.config dodać odpowiednią konfigurację database initializera:

<entityFramework>
<contexts>
<context type="EFMigrationExample.DataAccess.DataContext, EFMigrationExample.DataAccess">
<databaseInitializer type="System.Data.Entity.MigrateDatabaseToLatestVersion`2[[EFMigrationExample.DataAccess.DataContext, EFMigrationExample.DataAccess], [EFMigrationExample.DataAccess.Migrations.Configuration, EFMigrationExample.DataAccess]], EntityFramework" />
</context>
</contexts>
</entityFramework>
view raw web.config hosted with ❤ by GitHub

Takie rozwiązanie jest bardzo wygodne, ale też niesie za sobą trudności. Największym problemem jest to, że użytkownik, z którego korzystamy, w połączeniu do bazy danych musi mieć zwiększone uprawnienia, aby mógł zmienić schemat tej bazy. To niestety nie jest najlepszym rozwiązaniem, szczególnie gdy w aplikacji mamy podatność typu sql injection.

Innym rozwiązaniem jest skorzystanie z dedykowanego narzędzia dostarczonego wraz z Entity Framework. Mowa o migrate.exe (https://docs.microsoft.com/pl-pl/ef/ef6/modeling/code-first/migrations/migrate-exe). Narzędzie to można znaleźć w katalogu „packages\EntityFramework.6.2.0\tools”:

entity framework migration exe

Możemy skorzystać z tego narzędzia np. podczas buildu aplikacji. Wykonujemy wtedy migracje z wykorzystaniem zupełnie innego użytkownika (z odpowiednimi uprawnieniami), a nie użytkownika, którego używamy podczas działania aplikacji. Po więcej informacji odsyłam do dokumentacji – https://docs.microsoft.com/pl-pl/ef/ef6/modeling/code-first/migrations/migrate-exe.

Własny migrator

Ostatnim sposobem, który ostatnio wykorzystuję coraz częściej, jest stworzenie własnego migratora. Jest to prosta aplikacja konsolowa, do której mogę przekazywać różne parametry, aby odpowiednio zaktualizować aplikację.

Późniejsze użycie tej aplikacji jest bardzo podobne do użycia migrate.exe, ale daje też dużo większe możliwości. Mogę dodać parametr, który będzie decydował o tym, czy do bazy mają zostać dodane dane testowe, czy ma odbyć się jedynie aktualizacja schematu bazy. Mogę również dodać parametr, który odtworzy z backupu jedną z baz testowych, którą następnie podniesie do aktualnej wersji. Możliwości jest dużo.

Samo stworzenie takiej aplikacji jest bardzo proste. Wystarczy utworzyć nowy projekt aplikacji konsolowej oraz dodać do niego obsługę parametrów za pomocą CommandLineParser. W parametrze możemy przekazać connection stringa do bazy, którą chcemy zaktualizować:

public class Options
{
[Option('c', "connectionString", Required = true, HelpText = "The connection string to database that needs to be updated.")]
public string ConnectionString { get; set; }
}
view raw Options.cs hosted with ❤ by GitHub

Następnie w klasie Program odczytujemy parametry i wykonujemy aktualizację bazy:

class Program
{
static void Main(string[] args)
{
var result = Parser.Default.ParseArguments<Options>(args);
result
.WithParsed(r => Migrate(r));
}
private static void Migrate(Options options)
{
var configuration = new Configuration();//Klasa Configuration z projektu DataAccess z konfiguracją migracji
configuration.TargetDatabase = new DbConnectionInfo(
options.ConnectionString,
"System.Data.SqlClient");
var migrator = new DbMigrator(configuration);
MigratorLoggingDecorator logger = new MigratorLoggingDecorator(migrator, new MigrationLogger());
logger.Update();
}
}
view raw Program.cs hosted with ❤ by GitHub

Dodatkowy logger na końcu metody Migrate służy do zapisu logów do nLoga, który następnie zapisuje logi do pliku tekstowego oraz wyświetla je na konsoli. Sam logger wygląda tak:

public class MigrationLogger : System.Data.Entity.Migrations.Infrastructure.MigrationsLogger
{
private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
public override void Info(string message)
{
_logger.Info(message);
}
public override void Warning(string message)
{
_logger.Warn(message);
}
public override void Verbose(string message)
{
_logger.Trace(message);
}
}

Tak przygotowany migrator wystarczy uruchomić z wiersza poleceń, w którym przekażemy connection stringa do bazy, która ma zostać zaktualizowana:

EFMigrationExample.Migrator.exe -c Server=.\sqlexpress;Database=EFMigrationExample;Trusted_Connection=True;
view raw cmd hosted with ❤ by GitHub

Wynik działa migratora wygląda na przykład tak:

entity framework own migrator

Przykład

Na githubie (https://github.com/danielplawgo/EntityFrameworkMigrationExample) znajduje się przykład do wpisu. Connection string do bazy występuje w nim w dwóch miejscach: web.config aplikacji ASP.NET MVC oraz app.config migratora. Dodatkowo connection string jest również ustawiony jako parametr (właściwości projektu EFMigrationExample.Migrator oraz zakładka Debug). Więc jeśli chcesz uruchomić migratora, musisz ustawić tam również poprawnego connection stringa.

Podsumowanie

Mechanizm migracji schematu bazy danych jest ważnym elementem aplikacji. Gdy nie mamy go w projekcie, wtedy na ogół tracimy dużo sił i czasu, aby doprowadzać wszystko do porządku. Jest to istotne, szczególnie gdy wykorzystujemy wiele baz danych. Jedną z metod jest więc właśnie wykorzystanie migracji z Entity Framework.

Mechanizm ten jest jednym z przyjemniejszych sposób zarządzania schematem baz danych. Jego największym problemem jest natomiast konieczność wykorzystywania Entity Framework (a nie każdy chce go używać w swoim projekcie). Widziałem też kiedyś projekt, w którym Entity Framework został użyty do zarządzania schematem bazy danych, natomiast Dapper był używany do pracy z danymi w aplikacji.

W następnych dwóch wpisach pokażę Ci inne sposoby na migrację schematu bazy danych. W pierwszej kolejności zobaczysz Fluent Migratora.

Szkolenie Entity Framework Core

Szkolenie Entity Framework Core

Zainteresował Ciebie ten temat? A może chcesz więcej? Jak tak to zapraszam na moje autorskie szkolenie z Entity Framework Core.

1 thought on “Migracja schematu bazy danych w Entity Framework

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.