Při práci s databázemi v .NET je Entity Framework Core (EF Core) jedním z nejpopulárnějších nástrojů pro správu a manipulaci s daty. Tento nástroj umožňuje využívat objektově-relační mapování (ORM), které propojuje databázové tabulky s objekty ve vaší aplikaci. Při generování modelů z existujících databází můžeme pomocí příkazu dbcontext scaffold automaticky vygenerovat potřebné třídy. Tento proces je velmi užitečný, protože nám šetří čas při práci s databázemi a zároveň umožňuje udržet kód efektivní a konzistentní. Pojďme se podívat na jednotlivé kroky, jak tento proces funguje a co všechno bychom měli vzít v úvahu při generování a úpravách modelů.

Příkaz dbcontext scaffold slouží k vygenerování tříd na základě existující databáze. Při jeho použití je potřeba správně nastavit parametry, jako je například řetězec pro připojení k databázi, poskytovatel databáze, výstupní složka, namespace nebo použití datových anotací. Příkaz vypadá takto:

bash
dotnet ef dbcontext scaffold "Connection_String" Microsoft.EntityFrameworkCore.SqlServer --output-dir Models --namespace Northwind.Console.EFCore.Models --data-annotations --context NorthwindDb

Tento příkaz provádí několik důležitých akcí. V první řadě, je specifikováno, že používáme poskytovatele Microsoft.EntityFrameworkCore.SqlServer, což znamená, že připojujeme databázi SQL Server. Dále je určeno, že modely budou generovány do složky Models a že budou použity datové anotace místo Fluent API pro konfiguraci entit.

Pokud pracujeme s databází Azure SQL nebo Azure SQL Edge, musíme upravit řetězec pro připojení tak, aby odpovídal specifickému prostředí. To je důležité pro zajištění správné komunikace mezi aplikací a databází na těchto platformách.

Po dokončení scaffoldingu se vytvoří více než 25 tříd, které odpovídají tabulkám v databázi. Jednou z těchto tříd je například Category.cs, která reprezentuje řádek v tabulce Categories. U této třídy je důležité si všimnout několika klíčových vlastností, které jsou použity pro konfiguraci entit:

  1. Atribut [Index]: Tento atribut, který byl zaveden v EF Core 5.0, označuje vlastnosti, které by měly být indexovány. To je užitečné pro optimalizaci dotazů, zejména v případě, že s těmito vlastnostmi často pracujeme.

  2. Atribut [Key]: Tento atribut určuje primární klíč entity, což v tomto případě odpovídá vlastnosti CategoryId.

  3. Atribut [InverseProperty]: Tento atribut slouží k definování cizího klíče, který odkazuje na jinou entitu. V tomto případě je definován vztah mezi kategorií a produkty pomocí cizího klíče.

Pokud se podíváme na třídu ProductsAboveAveragePrice.cs, všimneme si, že reprezentuje řádek vrácený databázovým pohledem (view), nikoli tabulkou. Proto je tato třída označena atributem [Keyless], což znamená, že nemá primární klíč.

Důležitou součástí generovaných tříd je také DbContext třída, která je zodpovědná za správu entit a jejich propojení s databází. Třída NorthwindDb.cs je příkladem takové třídy. Tato třída obsahuje několik klíčových částí:

  • Konstruktory: Třída má dva konstruktory. Jeden bez parametrů a druhý, který umožňuje předat konfigurační možnosti, jako je například připojovací řetězec.

  • DbSet vlastnosti: Každá vlastnost typu DbSet<T> představuje tabulku v databázi. Tato vlastnost je automaticky generována pro každou tabulku v databázi a umožňuje snadnou manipulaci s daty.

  • Metoda OnConfiguring: Tato metoda se používá pro konfiguraci připojení k databázi. Pokud není připojení specifikováno, používá se výchozí řetězec připojení.

U generovaných modelů je také důležité poznamenat, že jsou použity datové anotace i Fluent API pro konfiguraci entit. To znamená, že některé aspekty modelů jsou definovány pomocí atributů přímo v třídách (např. [Key], [StringLength]), zatímco složitější konfigurace, jako jsou vztahy mezi entitami, jsou definovány pomocí Fluent API ve třídě OnModelCreating.

Pokud chcete mít plnou kontrolu nad konfigurací, můžete upravit metody OnModelCreating a OnConfiguring. Například v metodě OnModelCreating můžete přidávat nové vztahy mezi entitami nebo měnit chování existujících entit bez toho, abyste museli znovu generovat třídy.

Další důležitou součástí je správné nakládání s připojovacím řetězcem. Případné citlivé informace by měly být uloženy mimo zdrojový kód, ideálně v konfiguraci aplikace. Efektivní způsob správy připojovacích řetězců je použití konfiguračních souborů, což také zvyšuje bezpečnost aplikace.

Pokud se rozhodnete pro generování modelů z existující databáze, je důležité si být vědom několika věcí. Třídy, které jsou generovány, jsou pouze základními strukturami. Je nutné je upravit a přizpůsobit konkrétním potřebám vaší aplikace, například přidáním vlastních metod, validací nebo jinými úpravami, které zlepší funkčnost aplikace. Scaffoldování může také občas vést k neoptimálním názvům nebo nastavením, které je potřeba přehodnotit.

V závěru je třeba zmínit, že i když nástroje jako EF Core scaffold usnadňují práci s databázemi, vždy je potřeba rozumět tomu, jak tento nástroj funguje a jaké má limity. Při používání scaffoldingu je dobré mít na paměti možnost úpravy generovaných tříd podle potřeby, což může zahrnovat přidání vlastních konfigurací nebo změny ve struktuře modelů.

Jak správně mapovat dědičnost v relačních databázích pomocí SQL Serveru

Při modelování dědičnosti v relačních databázích je jedním z hlavních rozhodnutí výběr strategie mapování, která bude použita k implementaci hierarchie tříd. Ve světě SQL Serveru existují tři hlavní přístupy, jak tuto dědičnost řešit: TPH (Table-per-Hierarchy), TPT (Table-per-Type) a TPC (Table-per-Concrete-Type). Každá z těchto strategií má své výhody a nevýhody a je důležité vybrat správnou metodu v závislosti na konkrétních požadavcích aplikace a databázové architektury.

Table-per-Concrete-Type (TPC)

U této metody je pro každý konkrétní typ vytvořena samostatná tabulka. V případě hierarchie Person-Student-Employee, TPC použije samostatnou tabulku pro každý typ, jak je to uvedeno v následujícím příkladu kódu:

sql
CREATE TABLE [Students] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [PersonIds]), [Name] nvarchar(max) NOT NULL, [Subject] nvarchar(max) NULL, CONSTRAINT [PK_Students] PRIMARY KEY ([Id]), CONSTRAINT [FK_Students_People] FOREIGN KEY ([Id]) REFERENCES [People] ([Id]) ); CREATE TABLE [Employees] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [PersonIds]),
[Name] nvarchar(max)
NOT NULL, [HireDate] nvarchar(max) NULL, CONSTRAINT [PK_Employees] PRIMARY KEY ([Id]), CONSTRAINT [FK_Employees_People] FOREIGN KEY ([Id]) REFERENCES [People] ([Id]) );

V tomto příkladu je pro každý typ Student a Employee vytvořena samostatná tabulka, přičemž pro každý záznam je přiřazen unikátní identifikátor. Pro zajištění, že se ID hodnoty mezi tabulkami nebudou překrývat, používáme sekvenci:

sql
DEFAULT (NEXT VALUE FOR [PersonIds])

Výhody a nevýhody TPC

Hlavní výhodou této strategie je výkon. Když dotazujeme pouze jeden konkrétní typ, potřebujeme pouze jednu tabulku, což eliminuje nákladné spojování (joins). Tato metoda se ukazuje jako efektivní pro rozsáhlé hierarchie s mnoha konkrétními typy, kde každý typ má specifické vlastnosti.

Na druhou stranu hlavní nevýhodou je, že data jsou rozptýlena po více tabulkách, což může vést k obtížnému opětovnému sestavení dat. Tato metoda tedy není vždy ideální, zejména pokud by bylo třeba provádět složité dotazy mezi více tabulkami.

Jak nakonfigurovat mapování hierarchie

Při nastavení mapování dědičnosti v Entity Frameworku je nejprve nutné definovat všechny typy, jak je to uvedeno v následujícím příkladu:

csharp
public DbSet<Person> People { get; set; } public DbSet<Student> Students { get; set; } public DbSet<Employee> Employees { get; set; }

Pro použití výchozího mapování TPH není třeba provádět žádné speciální nastavení, protože je to implicitní metoda. Pokud chcete použít jiné metody, jako je TPT nebo TPC, můžete zavolat příslušnou metodu:

csharp
modelBuilder.Entity<Person>().UseTptMappingStrategy();
modelBuilder.Entity<Person>().UseTpcMappingStrategy();

Kromě toho můžete specifikovat název tabulky pro každou entitu:

csharp
modelBuilder.Entity<Student>().ToTable("Students"); modelBuilder.Entity<Employee>().ToTable("Employees");

Pro TPC strategii je potřeba nastavit také sdílenou sekvenci, aby byly hodnoty ID konzistentní napříč tabulkami:

csharp
modelBuilder.HasSequence("PersonIds");
modelBuilder.Entity<Student>().UseTpcMappingStrategy().Property(e => e.Id).HasDefaultValueSql("NEXT VALUE FOR [PersonIds]");

Praktická ukázka

Nyní si ukažme praktické použití této konfigurace. Začněte vytvořením aplikace Console App v rámci projektu, jak je uvedeno v následujících krocích:

  1. Vytvořte nový projekt Console App a přidejte potřebné balíčky pro EF Core.

  2. Vytvořte třídy Person, Student a Employee, kde každá z nich bude odvozena od základní třídy Person.

  3. Nakonfigurujte DbContext a v metodě OnModelCreating použijte správné mapovací strategie.

Příklad třídy Person:

csharp
public abstract class Person { public int Id { get; set; } [Required] [StringLength(40)] public string? Name { get; set; } }

Třídy Student a Employee dědí z Person a přidávají specifické vlastnosti:

csharp
public class Student : Person {
public string? Subject { get; set; } } public class Employee : Person { public DateTime HireDate { get; set; } }

Poté vytvořte třídu HierarchyDb, která bude obsahovat DbSet pro všechny typy:

csharp
public class HierarchyDb : DbContext {
public DbSet<Person> People { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Employee> Employees { get; set; }
public HierarchyDb(DbContextOptions options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Person>().UseTphMappingStrategy(); } }

Význam správné volby strategie

Je zásadní pochopit, že správná volba strategie mapování má přímý dopad na výkon a údržbu databáze. Použití TPC, i když zajišťuje dobrý výkon při dotazování konkrétních typů, může být nevhodné, pokud databáze obsahuje mnoho typů a komplexní dotazy mezi nimi. Naopak TPH, i když využívá jednu tabulku pro všechny typy, může vést k problémům s výkonem při složitých dotazech s velkými datovými sadami.

Při volbě mezi těmito strategiemi je tedy důležité zvážit nejen výkon, ale i údržbu databáze v dlouhodobém horizontu. Pokud je možné, vždy je dobré preferovat TPH, protože je efektivní a jednoduché na implementaci. TPC by mělo být použito v případech, kdy máte jasně oddělené konkrétní typy, každý s vlastními specifickými vlastnostmi, a kdy je výkon klíčový.