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 MyContext
u 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ů:
- Inicializace kontextu
- 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 DropCreateDatabaseIfModelChanges
odstraní 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:
- Na vhodném místě nastavíme initializer přes
Database.SetInitializer
- V určitý moment poprvé vytoříme instanci kontextu
new MyContext()
- Provede se konstruktor
MyContext
- V určitý moment se volá inicializace, např.:
context.Database.Initialize();
- Provede se metoda
InitializeDatabase()
v Initializeru - Provede se metoda
OnModelCreating
v MyContext - Provede se konstruktor v
Configuration
- 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í.