Projektowanie, Programowanie, Codzienność – BeniaminZaborski.com

30 Maj 2011

Entity Framework code-first – pierwsze koty za płoty

W ostatnim czasie pojawiło się kilka zmian w Entity Framework obok których nie sposób przejść obojętnie. Od dawna przyglądam się rozwojowi tego O/R mappera, ale jak dotąd nigdy nie byłem jeszcze tak podekscytowany jak teraz. Co takiego wywołuje ową ekscytację? Code-first! Tak to jest to. Z pełną świadomością mogę powiedzieć, że dla mnie code-first = Entity Framework. Wszystko co jest poza tym w EF już może nie istnieć. Ale postarajmy się to nieco uporządkować.

Entity Framework dostarcza trzy różne podejścia do odwzorowywania encji modelu domeny na relacyjne obiekty bazy danych:

– database-first: zaczynamy od utworzenia bazy danych, a EF automatycznie generuje klasy modelu na podstawie struktury bazy danych

– model-first: zaczynamy od utworzenia klas modelu w dedykowanym designerze i mapujemy je na istniejącą bazę danych lub EF generuje bazę na podstawie stworzonego modelu

– code-first: zaczynamy od utworzenia klas modelu wprost w kodzie i mapujemy na obiekty bazy danych.

W dalszej części zamierzam skupić się na tym ostatnim podejściu. Według mojego może odrobinę subiektywnego odczucia jest to jedyny wartościowy sposób użycia EF. Kontrowersyjne? Otóż nie do końca. Patrząc przez pryzmat programisty używającego przez kilka lat NHibernate (ostrzegałem, że może nie być obiektywnie) takie podejście wydaje się być zupełnie naturalne.

Rozpoczynam od utworzenia obiektów modelu domeny – obiektów POCO rzecz jasna – nie myśląc na tym etapie o projektowaniu relacji bazy danych (Domain Driven Development). W następnej kolejności tworzę odpowiednią strukturę bazy danych i mapuję na nią klasy modelu. Entity Framework potrafi sam wygenerować strukturę bazy danych na podstawie klas modelu stosując analogiczne nazewnictwo do nazw klas i ich właściwości. Wygodne rozwiązanie, jednak życie pokazuje, że mało praktyczne szczególnie w dużych projektach. Każdy z nas ma jakieś przyzwyczajenia co do nazewnictwa obiektów w bazie danych – ja również i one się nie pokrywają z konwencjami nazewniczymi dostarczanymi przez EF. Przyzwyczajenia owszem można zmienić, co jednak gdy korporacja posiada ścisłe reguły co do nazywania obiektów w bazie danych? Głową muru się nie przebije, stąd potrzeba elastycznego mechanizmu konfiguracji mapowania. Na początku się obawiałem na jak wiele pozwoli nam tutaj Microsoft. Niepotrzebnie, bo za pomocą fluent API można naprawdę dużo. Nie jest to co prawda tak dużo jak oferuje NHibernate, ale nie powinno to wpłynąć na wygodę używania Entity Framework Code-First.

Korzystając z podejścia code-first mamy trzy sposoby na mapowanie obiektowo-relacyjne. Po pierwsze, wspomniane już, automatyczne generowanie struktury bazy danych; po drugie mapowanie przy pomocy atrybutów z przestrzeni nazw System.Data.Annotations; i po trzecie preferowane przeze mnie ze względu na swoje możliwości – fluent API.

Nie uważam, aby mapowanie przy pomocy atrybutów było złym rozwiązaniem, po prostu fluent API wydaje mi się lepsze. Dzięki temu mogę odseparować model od bazy danych i stworzyć go bardziej „czystym”. Pod tym względem zdecydowanie jestem purystą.

Przeanalizujmy sposób użycia fluent API na podstawie projektu prostej aplikacji konsolowej stworzonej w Visual Studio, którą można pobrać stąd. Trywialny model domeny (mocno anemiczny) posiada jedynie cztery klasy. Nie w tym rzecz jednak. Różne relacje między tymi klasami obrazują to co najważniejsze w kontekście konfiguracji mappingu. Projekt pokazuje jak zmapować relacje między obiektami typu: many-to-one (Uzytkownik->Rola), one-to-many (Uzytkownik->Komputer) czy many-to-many (Rola->Uprawnienie).

Przejdźmy do tworzenia wspomnianego modelu domeny, dodając do projektu cztery następujące klasy:

public class Komputer

{

public long? Id { get; set; }

public string Nazwa { get; set; }

public string AdresIP { get; set; }

}

public class Uprawnienie

{

public long? Id { get; set; }

public string Nazwa { get; set; }

public IList<Rola> Role { get; set; }

}

public class Rola

{

public long? Id { get; set; }

public string Nazwa { get; set; }

public IList<Uprawnienie> Uprawnienia { get; set; }

}

public class Uzytkownik

{

public long? Id { get; set; }

public string Login { get; set; }

public bool CzyAktywny { get; set; }

public Rola Rola { get; set; }

public IList<Komputer> Komputery { get; set; }

}

Kolejnym krokiem jest wygenerowanie stosownej struktury tabel w bazie danych zgodnie z przyjętą przez siebie/organizację konwencją. Pomijam opis tego procesu, bo może być wykonany na wiele sposobów. Począwszy od „ręcznego” utworzenia tabel, aż po skorzystanie z własnych dedykowanych do tego narzędzi. Niejednokrotnie jest tak, że otrzymujemy już gotową strukturę bazy danych od programisty baz danych. W załączonym projekcie znajduje się skrypt SQL tworzący wymaganą strukturę bazy danych (dla MS SQL Server).

Mamy model domeny w postaci klas, mamy strukturę bazy danych – czas przejść do mapowania. Zanim jednak to zrobimy, dodajmy jeszcze connection string do naszej bazy danych do pliku konfiguracyjnego aplikacji app.config:

<connectionStrings>

<clear/>

<addname=ConnStr

connectionString=Data Source=(local);Initial Catalog=EF_CODE_FIRST_SAMPLE_DB;Integrated Security=SSPI;

providerName=System.Data.SqlClient />

</connectionStrings>

Kluczowym elementem aplikacji z wykorzystaniem Entity Framework code-first jest klasa DbContext z przestrzeni nazw System.Data.Entity. Pełni ona rolę wzorca Repository, a także jednocześnie Unit Of Work. To dzięki tej klasie możemy wykonywać na encjach modelu wszelkie operacie CRUD w ramach transakcji. Oznacza to, że naszym kolejnym krokiem jest utworzenie właśnie klasy dziedziczącej po DbContext. Zróbmy to:

public class ModelContext : DbContext

{

public ModelContext() : base() { }

public ModelContext(string stringNameOrConnectionString) : base(stringNameOrConnectionString) { }

}

Użyłem tu przeciążenia konstruktora, które pozwoli nam podać nazwę connection stringa z pliku konfiguracyjnego lub wprost podać connection string do bazy danych.

Kolejna bardzo istotna klasa, funkcjonująca nierozerwalnie z DbContext, to DbSet<>. Reprezentuje kolekcję encji danego typu w ramach kontekstu jaki definiuje nam klasa DbContext. Dodajmy zatem stosowne kolekcje encji do klasy ModelContext:

public DbSet<Rola> Role { get; set; }

public DbSet<Uzytkownik> Uzytkownicy { get; set; }

W tym momencie możemy przejść już do samego mapowania z użyciem fluent API. W związku z tym w klasie ModelContext musimy przesłonić metodę OnModelCreating:

protected override void OnModelCreating(DbModelBuilder modelBuilder)

{

base.OnModelCreating(modelBuilder);

}

To jest właśnie to miejsce w którym definiujemy konfigurację mappingu. Możemy to zrobić wprost w tej metodzie lub lepszym pomysłem będzie wydzielenie mappingu poszczególnych encji do dedykowanych klas. Sprawdza się to szczególnie w większych projektach. W tym celu należy utworzyć klasę dziedziczącą po EntityTypeConfiguration<> dla każdej encji:

public class KomputerMapping : EntityTypeConfiguration<Komputer>

{

public KomputerMapping()

: base()

{

this.HasKey(e => e.Id).Property(e => e.Id).HasColumnName(„ID_KOMPUTERA”);

this.Property(e => e.Nazwa).HasColumnName(„NAZWA”);

this.Property(e => e.AdresIP).HasColumnName(„ADRES_IP”);

this.ToTable(„KOMPUTERY”);

}

}

public class UprawnienieMapping : EntityTypeConfiguration<Uprawnienie>

{

public UprawnienieMapping()

: base()

{

this.HasKey(e => e.Id).Property(e => e.Id).HasColumnName(„ID_UPRAWNIENIA”);

this.Property(e => e.Nazwa).HasColumnName(„NAZWA”);

this.ToTable(„UPRAWNIENIA”);

}

}

public class RolaMapping : EntityTypeConfiguration<Rola>

{

public RolaMapping()

: base()

{

this.HasKey(e => e.Id).Property(e => e.Id).HasColumnName(„ID_ROLI”);

this.Property(e => e.Nazwa).HasColumnName(„NAZWA”);

this.HasMany<Uprawnienie>(u => u.Uprawnienia).WithMany(u => u.Role).Map(

m => m.ToTable(„UPRAWNIENIA_DLA_ROLI”)

.MapLeftKey(„ID_ROLI”)

.MapRightKey(„ID_UPRAWNIENIA”)

);

this.ToTable(„ROLE”);

}

}

public class UzytkownikMapping : EntityTypeConfiguration<Uzytkownik>

{

public UzytkownikMapping()

: base()

{

this.HasKey(e => e.Id).Property(e => e.Id).HasColumnName(„ID_UZYTKOWNIKA”);

this.Property(e => e.Login).HasColumnName(„LOGIN”);

this.Property(e => e.CzyAktywny).HasColumnName(„CZY_AKTYWNY”);

this.HasRequired(e => e.Rola).WithMany().Map(p=>p.MapKey(„ID_ROLI”));

this.HasMany(e => e.Komputery).WithOptional().Map(p => p.MapKey(„ID_UZYTKOWNIKA”)).WillCascadeOnDelete();

this.ToTable(„UZYTKOWNICY”);

}

}

To jest kompletny mapping dla naszego modelu domeny. Przyjrzyjmy się kluczowym elementom. Wywołanie HasKey() wskazuje które property jest primary key w naszej encji, natomiast Property() umożliwia zmapowanie konkretnej właściwości na konkretną kolumnę w tabeli bazy danych. Użyłem wywołania HasColumnName(), aby wskazać inną nazwę kolumny w tabeli – nieodpowiadającą konwencji nazewniczej EF. To wszystko okraszone jest wywołaniem ToTable(), gdzie wskazujemy nazwę tabeli odpowiadającą encji. Mapowanie klucza głównego i właściwości typów prostych wygląda na niezbyt skomplikowane. Czas przejść do mapowania relacji między obiektami.

Na pierwszy ogień many-to-one czyli zwykła referencja do innego typu encji z naszego modelu. Dobrym przykładem jest powiązanie użytkownika z rolą. Klasa Uzytkownik posiada referencję do klasy Rola, przy czym jest to referencja wymagana. Mapujemy to za pomocą wywołania HasRequired() wskazując nazwę właściwości przechowującej referencję do klasy Rola. W związku z tym, że także w tym wypadku konwencja nazewnicza klucza obcego w tabeli nie odpowiada tej z EF należy wskazać nazwę. Robimy to przy pomocy wywołania MapKey() podając nazwę klucza obcego w tabeli. Kompletny przykład mapowania takiej relacji wygląda następująco:

this.HasRequired(e => e.Rola).WithMany().Map(p=>p.MapKey(„ID_ROLI”));

W sytuacji kiedy property przechowujące referencję nie jest wymagane, to zamiast HasRequired() użyjemy HasOptional().

Innym typem relacji jest kolekcja czyli one-to-many. Przeanalizujmy to na przykładzie kolekcji elementów Komputer w klasie Uzytkownik. Do zmapowania listy używamy wywołania HasMany() podając jako parametr property – za pomocą wyrażenia lambda oczywiście – które wskazuje naszą kolekcję. W związku z tym, iż ta właściwość nie jest wymagana używamy WithOptional(). Jak się wszyscy domyślają w przeciwnym wypadku użylibyśmy WithRequired(). Wywołanie Map() z MapKey() wygląda już znajomo i używamy go by wskazać klucz obcy w tabeli zawierającej rekordy będące elementami listy. Dodatkowo na końcu znajduje się wywołanie metody WillCascadeOnDelete(). Cóż to takiego? Nic innego jak wskazanie czy podczas usunięcia elementu nadrzędnego ma być wywołane kaskadowe usunięcie elementów podrzędnych, czyli tych w naszej kolekcji. Cały przykład wygląda następująco:

this.HasMany(e => e.Komputery).WithOptional().Map(p => p.MapKey(„ID_UZYTKOWNIKA”)).WillCascadeOnDelete();

Na koniec najbardziej skomplikowana relacja czyli many-to-many. Przykładem takiego powiązania jest wzajemna relacja między obiektami Rola i Uprawnienie. Rola posiada kolekcję elementów Uprawnienie i vice versa, tj. Uprawnienie posiada kolekcję elementów Rola. Aby zmapować takie powiązanie na postać relacyjną potrzebujemy dodatkowej tabeli. Ta tabela przechowuje pary identyfikatorów roli i uprawnienia. Dobrze, aby kluczem głównym takiej tabeli była owa para identyfikatorów. Mapowania wykonujemy z jednej ze stron np. z encji Rola. Spójrzmy na przykład:

this.HasMany<Uprawnienie>(u => u.Uprawnienia).WithMany(u => u.Role).Map(

m => m.ToTable(„UPRAWNIENIA_DLA_ROLI”)

.MapLeftKey(„ID_ROLI”)

.MapRightKey(„ID_UPRAWNIENIA”)

);

Wywołujemy generyczną wersję metody HasMany, podając jako parametr generyczny typ elementu kolekcji, a jako parametr wywołania właściwość klasy przechowującą kolekcję tych elementów. Następnie w wywołaniu WithMany podajemy wskazanie w drugą stronę tj. w tym przypadku na kolekcję elementów Rola. Ostatni element mapowania relacji wiele do wielu i najważniejszy to wskazanie wspomnianej tabeli powiązania w Map(). Nazwę tabeli podajemy w wywołaniu ToTable i wskazujemy nazwy kolumn kluczy obcych do obu typów encji/tabel. Należy dodać, że left key to klucz obcy wskazujący na typ którego mapping opisujemy, a right key to klucz obcy wskazujący na typ elementów kolekcji.

Tak by wyglądał przyznaje, że dość pobieżny opis mapowania encji w moim prostym projekcie. Uważam, że fluent API jest na tyle intuicyjne i samo-opisujące iż czysty kod wielu z Was najlepiej pomoże uchwycić to wszystko.

Ostatnim krokiem jest wskazanie, które z klas „konfiguracyjnych” definiują mapping których encji. To bardzo proste – w metodzie OnModelCreating dodajemy:

modelBuilder.Configurations.Add(newRolaMapping());

modelBuilder.Configurations.Add(newUzytkownikMapping());

modelBuilder.Configurations.Add(newKomputerMapping());

modelBuilder.Configurations.Add(newUprawnienieMapping());

Zachęcam jeszcze do przeanalizowania kompletnego projektu zamieszczonego tutaj.

Na koniec nie mógłbym nie podkreślić tego, iż trzymam kciuki za dalszy rozwój Entity Framework właśnie w tym kierunku – jedynym słusznym kierunku.

23 czerwca 2010

Prośby zostały wysłuchane, czyli POCO w Entity Framework.

Filed under: Codzienne dylematy modelarza — Tagi: , , — Beniamin Zaborski @ 23:13

Jakiś czas temu pisałem na blogu czemu nie lubię Entity Framework, narzekałem i wytykałem błędy (zresztą nie byłem w tym sam). W tym momencie muszę przyznać, że Microsoft stanął na wysokości zadania i „naprawił” to co mnie najbardziej irytowało w Entity Framework. Tak – teraz encje są obiektami POCO, a sam Entity Framework ma cechy O/R Mappera w stylu persistence ignorant. Wszystko jest w najlepszym porządku. Dla tych, którzy nie widzą problemu w używaniu O/R Mappera, który nie jest persistence ignorant mam dobrą wiadomość, bo nadal EF może działać po „staremu” i domyślnie tak działa.

Aby jednak móc skorzystać z tego cudu jakim jest persistence ignorant w EF, w Visual Studio należy doinstalować odpowiedni generator. Ta operacja w Visual Studio 2010 jest dziecinnie prosta, gdyż wystarczy po utworzeniu modelu EF z menu kontekstowego designera encji wybrać „Add Code Generation Item…” -> Online Templates -> Templates -> Database -> „ADO.NET C# POCO Entity Generator”. Po zainstalowaniu dodatku zobaczymy w naszym solution pliki szablonów *.tt oraz związany z nimi custom tool TextTemplatingFileGenerator.

Dzięki szablonom i generatorowi możemy się cieszyć encjami POCO w EF. Obiekty encji teraz to czyste obiekty, które nie dziedziczą już ze specyficznej klasy bazowej jak to w „starym” EF było. Dodać należy jeszcze tylko tyle, że działa tu lazy loading oraz change tracking.

Jest dobrze, ale czy nie może być lepiej? Zawsze może! W związku z tym zawsze można wprowadzić modyfikacje do szablonów generatora, aby przystosować generowany kod jeszcze bardziej do własnych potrzeb. Zachęcam do zapoznania się z POCO Entity Generator dla Entity Framework. Hmm … może to właśnie EF kiedyś zastąpi NH w moich projektach (?).

Stwórz darmową stronę albo bloga na WordPress.com.