Code First initializers a migrace - kompletní přehled
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<Article> 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<DbContext> - DropCreateDatabaseIfModelChanges<DbContext> - DropCreateDatabaseAlways<DbContext>
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<MyContext>(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<MyContext> { 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, můžeme dostat něco podobného tomuto:
<Edmx Version="3.0" xmlns="http://schemas.microsoft.com/ado/2009/11/edmx"> <Runtime> <ConceptualModels> <Schema Namespace="Migrations" Alias="Self" annotation:UseStrongSpatialTypes="false" xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation" xmlns:customannotation="http://schemas.microsoft.com/ado/2013/11/edm/customannotation" xmlns="http://schemas.microsoft.com/ado/2009/11/edm"> <EntityType Name="Article" customannotation:ClrType="Migrations.Article, Migrations, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"> <Key> <PropertyRef Name="ArticleId" /> </Key> <Property Name="ArticleId" Type="Int32" Nullable="false" annotation:StoreGeneratedPattern="Identity" /> <Property Name="Title" Type="String" MaxLength="Max" FixedLength="false" Unicode="true" /> <Property Name="Length" Type="Int32" Nullable="false" /> <Property Name="Version" Type="Int32" Nullable="false" /> <Property Name="Name" Type="String" MaxLength="Max" FixedLength="false" Unicode="true" /> </EntityType> <EntityContainer Name="MyContext" customannotation:UseClrTypes="true"> <EntitySet Name="Articles" EntityType="Self.Article" /> </EntityContainer> </Schema> </ConceptualModels> <Mappings> <Mapping Space="C-S" xmlns="http://schemas.microsoft.com/ado/2009/11/mapping/cs"> ... // ZKRÁCENO
Tedy 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 sloupecName
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řída Configuration
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řebovat Seed()
metodu v Initializerech ale nově přetížíme metodou Seed()
přímo v Configuration.cs
:
public class Configuration : DbMigrationsConfiguration<MyContext> { 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.
add-migration InitialMigration Scaffolding migration 'InitialMigration'.
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:
201511161546256_InitialMigration.cs public partial class InitialMigration : DbMigration { public override void Up() { } public override void Down() { } }
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<MyContext> { 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říduConfiguration
- nemá vlastní metodu
Seed()
(spouštímeSeed()
třídyConfiguration
)
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<MyContext, Configuration> { 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í.