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 naší aplikaci a zajišťuje synchronizaci dat mezi databází a těmito objekty.
EF Entity Framework (EF) je open-source knihovna od Microsoftu, pomocí které můžeme přistupovat k podporovaným relačním databázím, například SQL Serveru, PostgreSQL, MySQL, Oracle a další.
Entita Entita je třída, kterou mapujeme na databázovou tabulku.
Navigation property Navigation property je vlastnost v Entitě, která reprezentuje relaci mezi tabulkama. 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 (ten 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 má dvojí syntaxi, buď můžeme použít dotazy podobné SQL nebo Fluent API. S EF se vhodně doplňuje.

Obsah

Úvod

Použití ORM je v dnešní době již standard a použití Entity Frameworku je zase běžné v prostředí .NET. V příkladě 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, protože umožňuje využít SSDT nástroje (což považuji za velkou výhodu), pokud se připojujeme k SQL Serveru. Použít můžeme také klasický Code First s novou databází, případně využít EF designer (varianta Model-First nebo Database-First) a nebo si nechat aplikační část vyscaffoldovat z již existující databáze pomocí reverse engineeringu. Možností jak mapování nastavit máme tedy několik. ORM nám mimo jiné může pomoci snížit závislost aplikace na použité databázové technologii, ale záleží na míře, ve které bychom použili specifické databázové funkce.

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 consolový projekt a nainstalujeme do něho EF balíček 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ě potřeba mít nastavené cizí klíče na straně databáze. Cizí klíče jsou velmi vhodné, někdy ale bohužel k dispozici nejsou. Díky tomu se například můžeme odkazovat i do tabulek 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á konvenci, musíme vytvořit další, který může být privátní a konvenci 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í žádoucí, aby EF používal veřejný konstruktor, pokud by například obsahoval nějaký další kód, například validace.
  • Pokud bychom v entitě chtěli použít vlastnost, kterou nemáme v tabulce, musíme použít ignore metodu nebo notmapped atribut.

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é již můžeme 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íč .HasKey(x => x.Id).
  • Pokud entita klíč nemá tak jí můžeme použít pouze pro čtení a musíme o tom EF informovat .HasNoKey().
Entity, které jsme si v předchozím kroku vytvořily tedy zaregistrujeme následovně (pokud bychom měli hodně tabulek nebo složitější nastavení, můžeme použít modelBuilder.ApplyConfiguration() nebo modelBuilder.ApplyConfigurationsFromAssembly() metody, které nám umožní přesunout nastavení z OnModelCreating() metody do separátní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 máme konzolový projekt, tak instanci databázového kontextu provedeme následovně

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í Include a ThenInclude metod.
  • Pokud bysme chtěli tabulky načíst pouze pro čtení, můžeme použít AsNoTracking() metodu.
SQL dotazy se posílají do databáze v momentě kdy voláme nad DbSet objektem metody jako ToList, First, Single, v případě procházení pomocí foreach konstrukce nebo při zavolání context.SaveChanges metody, která nám změny uloží do databáze.

Následující příklad demonstruje nahrání dat do databáze, jejich přečtení s vykreslením do konzole a smazání dat pomocí přímého SQL dotazu.

Závěr

O Entity Frameworku bylo napsáno mnoho knížek, ale snažil jsem se popsat základní přehled jak ORM nastavit a použít na příkladě, který obsahuje více než dvě tabulky 😅. Snad jsem neopomněl nic důležitého a uvidíme se u dalšího článku 🚀.

Odkazy