Code First initializers a migrace - kompletní přehled

Miroslav Holec       17.11.2015       Entity Framework, ASP.NET/IIS, .NET       11801 zobrazení

Entity Framework: Code First nabízí celou řadu možností, jak inicializovat databázi a spravovat ji vzhledem k aplikačnímu kódu. V tomto článku objasním všechny možnosti a principy, které Code First nabízí.

Na úplný začátek se hodí zmínit, že při práci s Code First řešíme v souvislosti s tímto tématem tři klíčové problémy:

  • problém inicializace databáze a seedování dat
  • problém migrací (automatických nebo manuálních)
  • problém aplikace změn aplikačního kódu proti databázi

Přestože tyto problémy spolu úzce souvisí, doporučuji se nad každým zamyslet samostatně.

DbContext

Pokud v našem aplikačním kódu potřebujeme pracovat s databází s využitím Code First, určitě na nějakém místě v kódu budeme mít něco podobného:

using (var context = new MyContext())
{
    var articles = context.Articles.ToList();
}

V praxi je samozřejmě lepší řídit životnost kontextu nějakým IoC kontejnerem, než jej vytvářet v průběhu requestu neustále dokola. DbContext může vypadat pro naše účely takto:

public class MyContext : DbContext
{
    public DbSet
Articles { get; set; } public MyContext() { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { } }

Při vytváření instance MyContextu se provede pouze konstruktor. Nic více. Chybná je úvaha, že se poté spustí automaticky metoda OnModelCreating().

Initializers

Samotný model vzniká až ve chvíli, kdy je to skutečně potřeba. Obvykle když se vývojář poprvé dotáže na data nebo pokud explicitně požádá o inicializaci databáze. Slouží k tomu metoda Initialize() a její použití osobně doporučuji (před náhodnou inicializací někde v kódu).

using (var context = new MyContext())
{
    context.Database.Initialize(false);
    ...

Jediným vstupním parametrem metody Initialize() je force(bool). Pokud je "true", inicializace proběhne za všech okolností a tedy i za předpokladu, že již jednou proběhla. Standardně se doporučuje tuto možnost nepovolovat. Proces inicializace se skládá ze dvou kroků:

  1. Inicializace kontextu
  2. Inicializace databáze

Během procesu inicializace kontextu je volána výše zmíněná metoda OnModelCreating(), kterou vývojář používá na definování vztahů v modelu mezi entiami, různá omezení nad atributy atd.

Proces inicializace databáze následuje hned poté a má za úkol reflektovat stav modelu proti fyzické databázi s využití tzv. initializeru. Výchozím initializerem je CreateDatabaseIfNotExists<DbContext>. Vývojář má nicméně k dispozici 3 inicializery, které může použít:

- CreateDatabaseIfNotExists
- DropCreateDatabaseIfModelChanges
- DropCreateDatabaseAlways

Každý dělá jednoduše to, co má uvedené ve svém názvu. Tedy DropCreateDatabaseIfModelChangesodstraní a znovu vytvoří databázi pokaždé, když se změní model. DropCreateDatabaseAlways odstraní a znovu vytvoří databázi pokaždé. Pokaždé je samozřejmě míněno pokaždé, když proběhne inicializace. A to se stává například při recyklaci aplikačního poolu (a pouze jednou pro aplikační doménu). Na vývojářském stroji tedy prakticky neustále s každou kompilací projektu. Právě proto se tyto tři inicializery používají nejčastěji v rané fázi vývoje aplikace.

Jejich použití je intuitivní:

public class MyContext : DbContext
{
    public MyContext()
    {
        Database.SetInitializer(new DropCreateDatabaseAlways());
        ...

Přestože řada vývojářů tento inicializer nastavuje v konstruktoru DbContextu (viz. příklad), mnohem lepším místem je inicializátor celé aplikace. Může to být metoda Main(), global.asax.cs nebo moment sestavování závislostí v IoC kontejneru. Záleží vždy na potřebách vývojáře a konkrétním nastavení.

Inicializer jako takový může vývojář dědit a vytvořit si tak vlastní. V praxi se to hodí kvůli přetížení metody Seed(), která umožňuje provést libovolný kód pro přidání nebo správu dat v databázi. Přetížit lze i metodu InitializeDatabase().

public class MyInitializer : DropCreateDatabaseIfModelChanges
{
    public override void InitializeDatabase(MyContext context)
    {
        base.InitializeDatabase(context);
    }
 
    protected override void Seed(MyContext context)
    {
        base.Seed(context);
    }
}

Dodejme, že metoda InitializeDatabase() se vykoná při každé inicializaci (viz. například zmíněná recyklace poolu). Oproti tomu Seed() se spustí pouze když je detekována změna v modelu (aplikačním kódu oproti DB).

Detekce změn v modelu

Pokud má všechno fungovat spolehlivě, musí být Entity Framework schopen detekovat změny, které v modelu nastaly. Pokud takovou změnu EF odhalí, pak jednoduše databázi odstraní a znovu vytvoří. Co ale v případě, kdy dojde ke změně a vývojář použije inicializer CreateDatabaseIfNotExists<MyContext>? V ten moment bude od nás EF chtít, abychom srovnali verzi aplikačního kódu a fyzického db modelu pomocí Code First Migrations.

The model backing the 'MyContext' context has changed since the database was created. Consider using Code First Migrations to update the database ( http://go.microsoft.com/fwlink/?LinkId=238269).

Pro udržování informací o verzích kontextu EF používá tabulku [__MigrationHistory], která obsahuje mimo jiné i důležitý sloupec Model. Nejedná se o Hash, jak se často vývojáři domnívají, ale o binární data vytvořená pomocí GZip komprese. Když taková data dekomprimujeme, dostaneme v podstatě klasické XML s informacemi o modelu. Když to shrneme, pro správnou synchronizaci aplikačního kódu a samotné databáze potřebuje EF Code First tzv. migrace. Ty jsou poté uchovávány v databázi a EF se na základě aktuálního stavu a initializeru rozhoduje, co bude dělat.

V praxi to také znamená, že Entity Framework nezajímá, zda se v databázi děje něco dalšího bez jeho vědomí. Zcela bez problémů může vývojářský tým přidávat do téže databáze vlastní tabulky nebo i vlastní sloupce nad tabulkami, které jsou generovány na základě aplikačního kódu. Je nutné ale myslet na to, že při pozdější aktualizaci nesmí žádná z těchto změn "překážet".

Co když ale vývojář nechce Entity Frameworku přenechat zodpovědnost za detekci změn a chce synchronizovat databázi s aplikačním kódem sám?

Reverse engineering

Jedna z možností, která se vývojáři nabízí je tabulku __MigrationHistory zcela odstranit a přepnout se tak do módu Reverse Engineering. Před stejnou situaci se dostaneme, pokud máme již existující databázi a píšeme (nebo generujeme) k ni zpětně kód (nezaměňovat s database first). V rámci tohoto módu EF předpokládá, že synchronizaci aplikačního kódu a fyzického modelu databáze má vývojář výhradně a zcela pod svou kontrolou.

Výhodou v tomto případě je, že se nikdy nedočkáme hlášky:

The model backing the 'MyContext' context has changed since the database was created. Consider using Code First Migrations to update the database ( http://go.microsoft.com/fwlink/?LinkId=238269).

a tudíž ani situace, kdy se stane celá aplikace náhle nepoužitelnou. Na druhou stranu je na vývojáři, aby si hlídal každý databázový sloupec a v případě opomenutí jakékoliv změny se může stát, že aplikace bude žít s bugem v produkci až do doby, než jej někdo odhalí.

Migrace

Z čistě akademického hlediska není migrace nic jiného než popis změn mezi verzemi aplikačního kódu, který má důsledek na fyzické schéma databáze. Migrace je tedy pouze informace, že v důsledku přidání nové property Name na třídě Articles budeme chtít generovat sloupec Name i v databázové tabulce.

Migrace můžeme provádět buď ručně a nebo zcela automaticky. Ještě jednou zdůrazňuji, že automatická migrace NEZNAMENÁ automatickou aktualizaci DB schématu.

Povolení migrací

Migrace je nutné v projektu nejprve povolit. Učinit tak můžeme pomocí Package Manager Console proti projektu, ve kterém se nachází kontext. Příkaz je prostoduchý:

PM> enable-migrations
Code First Migrations enabled for project Migrations.

S vytvořením migrací v projektu vzniká i nová složka Migrations se souborem Configuration.cs. TřídaConfiguration dědí od DbMigrationsConfiguration<MyContext> a nabízí konstruktor, kde lze definovat chování migrací. S povolením migrací dochází ještě k té změně, že již nebudeme potřebovatSeed() metodu v Initializerech ale nově přetížíme metodou Seed() přímo v Configuration.cs:

public class Configuration : DbMigrationsConfiguration
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = false;
    }
 
    protected override void Seed(MyContext context)
    {
    }
}

Migrace jsou ve výchozím stavu manuální. Pokud chce vývojář vytvořit novou migraci, volá příkaz Add-Migration <name> v NPM.

201511161546256_InitialMigration.cs
 
public partial class InitialMigration : DbMigration
{
    public override void Up()
    {
    }
 
    public override void Down()
    {
    }
}

Každá migrace je reprezentována kódem, který umožňuje později i přechod o verze zpět. Kód se nachází v souborech, které jsou generovány do složky Migrations. Každý soubor se skládá z timestampu a názvu samotné migrace. Příklad:

add-migration InitialMigration
Scaffolding migration 'InitialMigration'.

Tento kód může vývojářský tým sdílet skrze verzovací systém. Za určitých okolností (speciálně ve větších týmech) ale může docházet ke konfliktům verzí v důsledku paralelního vývoje modelu. V produkčním prostředí je vznik takových chyb vzácnější. Tomuto problému se věnuje do hloubky článek na MSDN:

Vraťme se ale k našemu aktuálnímu nastavení a shrňme si, co se bude nyní dít po spuštění aplikace:

  1. Na vhodném místě nastavíme initializer přes Database.SetInitializer
  2. V určitý moment poprvé vytoříme instanci kontextu new MyContext()
  3. Provede se konstruktor MyContext
  4. V určitý moment se volá inicializace, např.: context.Database.Initialize();
  5. Provede se metoda InitializeDatabase() v Initializeru
  6. Provede se metoda OnModelCreatingv MyContext
  7. Provede se konstruktor v Configuration
  8. Provede se metoda Seed v Configuration, pokud došlo ke změně modelu

Automatické migrace

Pokud si vzpomínáte na mou "akademickou" definici migrace, záměrně jsem nezmínil nic o tom, že by měla mít fyzickou reprezentaci. Pokud povolí vývojář v konstruktoru třídy Configuration automatické migrace

public class Configuration : DbMigrationsConfiguration
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = true;
    }
}

pak nebudou vznikat ve složce Migrations žádné migrační soubory. S každou aktualizací databáze (nebo pokusem o aktualizaci) se porovná aktuální aplikační kód (model) s fyzickým schématem databáze a v případě změny se provede například aktualizace databáze nebo vygenerování změnového skriptu.

Aktualizace databáze

Konečně jsme u posledního tématu, kterým je aktualizace samotné databáze. Jestliže se vyvíjí aplikace v režimu Reverse Engineering, pak nám Code First v podstatě nenabízí moc možností a aktualizace je pouze v našich rukách.

V případě manuálních migrací se aktualizace provádí na základě dostupných migračních souborů a v případě migrací automatických se porovná přímo aktuální stav aplikačního kódu s tabulkou __MigrationHistory. Existuje několik možností, jak migrace propsat do databáze.

Update-Database

Obecně nejznámnějším řešením je ruční aktualizace z NPM pomocí příkazu Update-Database.

PM> update-database
Specify the '-Verbose' flag to view the SQL statements being applied to the target database.
Applying explicit migrations: [201511161546256_InitialMigration].
Applying explicit migration: 201511161546256_InitialMigration.
Running Seed method.

Takto spuštěný příkaz generuje SQL scripty a ty rovnou aplikuje proti té databázi, pro kterou je aktuálně nastaven connection string k danému kontextu. Pokud se narazí na nějaký problém při provádění SQL scriptu, změny se neaplikují a vývojář musí problém ručně dořešit na základě sdělení z NPM. V takovém případě doporučuji druhé dostupné řešení.

Script generation

Druhé řešení je nepřenechat NPM spouštění SQL scriptů ale pouze si SQL kód nechat vygenerovat. Mnoho vývojářů volí tuto možnost, protože

  • chyby vrácené SQL serverem jsou mentálně srozumitelnější
  • aktualizace je pod kontrolou vývojáře a ten se může rozhodnout, kdy ji provede (kdy spustí SQL skript)
  • SQL skripty lze ještě ručně doupravit podle potřeby
  • SQL skripty lze uchovávat, sdílet a rozumí jim každý, kdo zná jazyk SQL (oproti C# kódu ve složce Migrations)

Pro vygenerování stačí použít příkaz: update-database -Script jehož výsledkem je něco ve smyslu:

ALTER TABLE [dbo].[Articles] ADD [Name] [nvarchar](max)
INSERT [dbo].[__MigrationHistory]([MigrationId], [ContextKey], [Model], [ProductVersion])
VALUES (N'201511161612190_Koko', N'Migrations.Migrations.Configuration',  0x1F8B08 !!!ZKRÁCENO!!! , N'6.1.3-40302')

 

MigrateDatabaseToLatestVersion

Vrcholem automatizace je použití initializeru s názvem MigrateDatabaseToLatestVersion. Ten se od třech doposud zmíněných liší v tom, že:

  • potřebuje znát nejen DbContext ale i třídu Configuration
  • nemá vlastní metodu Seed() (spouštíme Seed() třídy Configuration)

V praxi pak tento initializer aktualizuje databázi vždy, pokud

  • jsou povoleny automatické migrace a došlo ke změně kódu vůči __MigrationHistory
  • existují čekající manuální migrace (pending changes nad soubory v Migrations)

Vlastní a nejjednodušší implementace může vypadat takto:

public class LatestVersion : MigrateDatabaseToLatestVersion
{
    public override void InitializeDatabase(MyContext context)
    {
        base.InitializeDatabase(context);
    }
}

Přestože řada vývojářů tento Initializer nedoporučuje používat, v praxi s ním mám velmi dobré zkušenosti i v produkčním prostředí.

Migrate.exe

Kromě možnosti aktualizace databáze ručně a v prostředí Visual Studia existuje ještě nástroj migrate.exe, který je součástí balíčku EntityFramework staženého z NuGetu. Naleznete jej ve složce\packages\EntityFramework.6.1.3\tools. Více o tomto nástroji se lze dočíst na MSDN

 

Závěr

Cílem článku bylo popsat především základní principy generování fyzického datového modelu na základě aplikačního kódu pomocí EF Code First. Z vlastní zkušenosti obvykle začínám při vývoji nových projektů pracovat s prvními uvedenými initializery, na které navazuji spuštěním automatických migrací a automatickou aktualizací databáze pomocí MigrateDatabaseToLatestVersion initializeru. Teprve ve specifických případech pak přístupuji k manuálnímu řízení verzí.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Mohlo by vás také zajímat

Genericita, rozhraní a dědičnost

Jazyk C# je multiparadigmatický, což v praxi znamená, že v něm můžeme dělat hodně věcí. Jak ale do sebe jednotlivá paradigma zapadají? Co se hezky doplňuje a co není vzájemně kompatibilní? V tomto článku chci popsat, jak se chová IEquatable vzhledem k dědičnosti typu T.

Windows 10 GameJam: konference a herní hackathon

Visual Studio 2017 je venku!

 

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback