Objektově relační mapování a Entity Framework

15.05.2021

ORM nám umožňuje pracovat s databází objektovým přístupem. Článek se věnuje základní konfiguraci a použití.

Slovníček

ORM Objektově-relační mapování (ORM) je technika, která převádí hodnoty uložené v relační databázi na objekty v aplikaci a zajišťuje synchronizaci dat mezi databází a těmito objekty.
EF Entity Framework (EF) je open-source knihovna od Microsoftu, která umožňuje přistupovat k podporovaným relačním databázím, jako jsou SQL Server, PostgreSQL, MySQL, Oracle a další.
Entita Entita je třída, kterou mapujeme na databázovou tabulku.
Navigation property Navigační vlastnost (navigation property) je vlastnost entity, která reprezentuje relaci mezi tabulkami. Pomocí navigační vlastnosti můžeme do objektu načíst související data, na která se v databázi odkazujeme (ideálně cizím klíčem, který ale není povinný).
LINQ Jedná se o dotazovací jazyk integrovaný do C#, pomocí kterého můžeme pracovat s SQL daty, ADO.NET datasety, XML dokumenty, .NET kolekcema apod. LINQ nabízí dvojí syntaxi: lze použít dotazy podobné SQL nebo Fluent API. S EF tvoří vhodný doplněk.

Obsah

Úvod

Použití ORM je dnes již standardem a Entity Framework je běžnou volbou v prostředí .NET. V příkladu budu používat aktuální verzi EF Core 5 ve variantě Code First s existující databází, kterou preferuji pro stávající i nové projekty. Tato varianta umožňuje využít nástroje SSDT, pokud se připojujeme k SQL Serveru.

K dispozici jsou i další možnosti, například:

ORM nám také pomáhá snížit závislost aplikace na konkrétní databázové technologii, i když tato výhoda závisí na míře použití specifických funkcí dané databáze.

Příprava prostředí

Připravíme si databázi, která bude simulovat jednoduchý objednávkový systém a poběží nám na SQL Serveru.

create database OrderSystem;

use OrderSystem;
go

create table Customer
(
	Id int identity not null,
	FirstName nvarchar(255) not null,
	LastName nvarchar(255) not null,
	constraint PK_Customer_Id primary key (Id)
);

create table [Address]
(
	CustomerId int not null,
	City nvarchar(255) not null,
	Street nvarchar(255) not null,
	PostalCode nvarchar(255) not null,
	constraint PK_Customer_CustomerId primary key (CustomerId),
	constraint FK_Customer_CustomerId foreign key (CustomerId) references Customer (Id)
);

create table [Product]
(
	Id int identity not null,
	Number int not null,
	[Name] nvarchar(255) not null,
	constraint PK_Product_Id primary key (Id),
);

create table [Order]
(
	Id int identity not null,
	CustomerId int not null,
	[Status] int not null,
	OrderDate DateTime2(0) not null,
	constraint PK_Order_Id primary key (Id),
	constraint FK_Order_CustomerId foreign key (CustomerId) references Customer (Id)
);

create table [OrderLine]
(
	OrderId int not null,
	ProductId int not null,
	Quantity int not null,
	UnitPrice decimal(10,2) not null,
	constraint PK_OrderLine_OrderId_ProductId primary key (OrderId, ProductId),
	constraint FK_OrderLine_OrderId foreign key (OrderId) references [Order] (Id),
	constraint FK_OrderLine_ProductId foreign key (ProductId) references Product (Id)
);

Konfigurace

Vytvoříme si konzolový projekt a nainstalujeme do něj balíček EF s podporou SQL Serveru.

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

Vytvoření entit

Vytvoříme si třídy, které budou "reprezentovat" databázové tabulky.

  • Třída nemusí mapovat všechny sloupce z tabulky.
  • Pro použití navigačních vlastností není nutné mít nastavené cizí klíče na straně databáze. Cizí klíče jsou velmi vhodné, ale někdy bohužel nejsou k dispozici. Díky tomu se například můžeme odkazovat i na tabulky v jiné databázi pomocí synonym objektu.
  • Navigační vlastnosti jsou volitelné a mapují se buď automaticky podle konvence nebo, v případě nestandardní jmenné konvence, manuálně pomocí data anotací nebo Fluent API.
  • Pokud entita obsahuje konstruktor, který neodpovídá konvencím, musíme vytvořit další, který může být privátní a konvencím odpovídá. Osobně vytvářím vždy prázdný privatní konstruktor a nemusím to již v průběhu projektu řešit. Také není vhodné, aby EF používal veřejný konstruktor, pokud obsahuje další kód, například validace.
  • Pokud bychom v entitě chtěli použít vlastnost, která není součástí tabulky, musíme použít metodu Ignore nebo atribut NotMapped.

Nastavení DbContext třídy

  • Konfigurace EF a mapování entit na tabulky probíhá ve třídě, která dědí z tzv. DbContext třídy.
  • Entity se mapují do tzv. DbSet objektů, které lze následně použít pro práci s daty.
  • Každá entita, nad kterou budeme chtít provádět DML operace, musí mít nastavený primární klíč pomocí .HasKey(x => x.Id).
  • Pokud entita primární klíč nemá, lze ji použít pouze pro čtení, což musíme EF oznámit pomocí .HasNoKey().
Entity, které jsme si vytvořili v předchozím kroku, zaregistrujeme následujícím způsobem. Pokud máme větší počet tabulek nebo složitější nastavení, můžeme použít metody modelBuilder.ApplyConfiguration() nebo modelBuilder.ApplyConfigurationsFromAssembly(), které umožňují přesunout konfiguraci z metody OnModelCreating() do samostatných tříd.

using EFCode.Model;
using Microsoft.EntityFrameworkCore;

namespace EFDemo.Database 
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.HasDefaultSchema("dbo");

            modelBuilder
                .Entity<Customer>()
                .ToTable("Customer")
                .HasKey(x => x.Id);

            modelBuilder
                .Entity<Address>()
                .ToTable("Address")
                .HasKey(x => x.CustomerId);

            modelBuilder
                .Entity<Product>()
                .ToTable("Product")
                .HasKey(x => x.Id);

            modelBuilder
                .Entity<Order>()
                .ToTable("Order")
                .HasKey(x => x.Id);

            modelBuilder
                .Entity<OrderLine>()
                .ToTable("OrderLine")
                .HasKey(x => new { x.OrderId, x.ProductId });
        }
        public DbSet<Customer> Customers { get; set; }
        public DbSet<Address> Addresses { get; set; }
        public DbSet<Product> Products { get; set; }
        public DbSet<Order> Orders { get; set; }
        public DbSet<OrderLine> OrderLines { get; set; }
    }
}

Použití

Konfiguraci máme hotovou a můžeme se připojit k databázi. V našem případě, kdy pracujeme s konzolovým projektem, vytvoříme instanci databázového kontextu následujícím způsobem:

var connectionString = @"Server=localhost;Database=OrderSystem;Trusted_Connection=True;";

var optionsBuilder =
    new DbContextOptionsBuilder<AppDbContext>()
        .UseSqlServer(connectionString);

using var context = new AppDbContext(optionsBuilder.Options);

V případě ASP.NET Core frameworku pak můžeme použít dependency injection (DI), kdy v souboru Startup.cs do metody ConfigureServices() přidáme

services.AddDbContext<AppDbContext>(
    options => options.UseSqlServer(connectionString));

  • Pro čtení dat můžeme použít LINQ
  • Tabulky můžeme joinovat, díky navigačním vlastnostem, pomocí metod Include a ThenInclude.
  • Pokud bychom chtěli tabulky načíst pouze pro čtení, můžeme použít metodu AsNoTracking().
SQL dotazy se odesílají do databáze ve chvíli, kdy voláme nad objektem DbSet metody jako ToList(), First(), Single(). K odeslání dotazů také dochází při procházení kolekce pomocí konstrukce foreach nebo při zavolání metody context.SaveChanges(), která uloží provedené změny do databáze.

Následující příklad ukazuje, jak nahrát data do databáze, přečíst je a zobrazit v konzoli, a také jak data smazat pomocí přímého SQL dotazu:

Závěr

O Entity Frameworku bylo napsáno mnoho, ale snažil jsem se poskytnout základní přehled o tom, jak ORM nastavit a použít na příkladu, který zahrnuje více než dvě tabulky. 😅 Snad jsem nezapomněl na nic důležitého a těším se na setkání u dalšího článku. 🚀

Odkazy