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.

3 komentarzy »

  1. Witam,

    Zaprojektowałem swój model, wstawiam dane, relacje się uzupełniają – wszystko ładnie działa. Jednak po zamknięciu aplikacji i uruchomieniu ponownie, dane pochodzące z relacji 1 do n są nie dostępne, zamiast kolekcji obiektów mam null. (Rekordy w bazie danych i odpowiednie klucze dodawane są poprawnie)

    Postanowiłem poszukać przykładu jak to powinno wyglądać i natknąłem się na ten blog. Ściągnąłem kod i uruchomiłem. Wygląda ze działa, ale zrobiłem podobne wywołanie jak u mnie czyli, uruchamiam aplikację i odpytuje o konkretne dane, dostaje je , ale dane powiązane z konkretnym rekordem relacją już nie.

    Czy mógłby mi Pan pomóc, oto podobne wywołanie bazujące na Pana przykładzie?

    private static test()
    {
    ModelContext model = new ModelContext(„ConnStr”);
    Uzytkownik uz = model.Uzytkownicy.FirstOrDefault(); // to dostaje poprawnego uźytkownika z bazy
    int c = uz.Komputery.Count(); // tu dostaje 0 – choc w bazie przypisanych juz klika komputerow do tego uzytkownika
    Komputer komputer1 = new Komputer();
    komputer1.Nazwa = „Netbook”;
    komputer1.AdresIP = „127.0.0.1”;
    uz.Komputery.Add(komputer1);
    model.SaveChanges();
    c = uz.Komputery.Count(); // ale tu dosatje już 1

    //Po ponownym uruchomieniu jest dokladnie to samo, czyli 0 a rekordów w bazie danych przybywa
    }

    Pozdrawiam serdecznie
    Arek

    Komentarz - autor: Arek — 21 Grudzień 2012 @ 14:32

    • Witam
      Nie wiem czy w międzyczasie poradził sobie Pan z tym problemem, ale spróbuję pomoc.
      Pana problem jest związany z tym iż domyślnie w EF mechanizm lazy loading jest wyłączony. W związku z tym aby została załadowana właściwość referencyjna należy o tym poinformować EF za pomocą Include. W Pana przykładzie wyglądało by to następująco:

      Uzytkownik uz = model.Uzytkownicy.Include(„Komputery”).FirstOrDefault();

      Innym rozwiązaniem jest włączenie mechanizmu lazy loadingu w następujący sposób:

      model.ContextOptions.LazyLoadingEnabled = true;

      Pozdrawiam
      Beniamin

      Komentarz - autor: Beniamin Zaborski — 7 Luty 2013 @ 19:26

  2. Poprawiłem link do przykładowego kodu źródłowego!

    Komentarz - autor: Beniamin Zaborski — 11 Czerwiec 2014 @ 06:32


RSS feed for comments on this post. TrackBack URI

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s

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

%d bloggers like this: