Entity Framework dla początkujących

1. Co to jest Entity Framework i do czego go używamy?

 

Entity Framework jest czwartą najczęściej pobieraną biblioteką ze strony NuGet.org, zaraz po jQuery oraz bibliotekami ASP.NET oraz Newtonsoft.JSON

Jest on bardzo dobrym ORM (Object-Relational Mapping), przeznaczonym do budowania aplikacji opartych o bazy danych. Najczęściej wykorzystywany w aplikacjach przechowywujących i zarządzających danymi, możemy używać go tak w VB.NET, jak i w C#.

Do czego może mi się to przydać?

Entity Framework jest przydatny do wielu rzeczy, jednak główną jego zaletą z punktu widzenia programisty jest możliwość „patrzenia” na bazę danych jak na zlepek obiektów klas naszego programu.
Przygodę z tą biblioteką możemy rozpocząć od małego przykładu, w jaki sposób możemy działać na bazie danych i w jaki sposób możemy ją stworzyć od podstaw.

 

 

2. Wstęp do Entity Framework Code First

 

Co to jest Code First?

Jak może sugerować sama nazwa, „Code First” oznacza tworzenie bazy danych za pomocą odwzorowania jej tabel przez klasy w naszym programie. W naszym przypadku – stworzymy prosty system katalogowania płyt CD naszych ulubionych zespołów.

Tak więc – tworzymy nasze klasy, ja na potrzeby poradnika, stworzę póki co tylko jedną klasę, ponieważ samymi kluczami obcymi i innymi rzeczami zajmiemy się w dalszej części poradnika. Póki co chcemy tylko poznać filozofię działania i sposób tworzenia bardzo podstawowej bazy danych.

 

2.1 Tworzenie bardzo prostej struktury bazy danych

 

Na początku otwieramy nowy projekt w Visual Studio, ja używam 2015, obecnie najnowszego wydania, jednak od wersji 2012 nie różnią się one zbyt bardzo od siebie, więc możecie używać dowolnej, nowszej od 2010.

Tworzymy solucję o dowolnej nazwie, w moim przypadku jest to „EntityFrameworkTutorial”. Zawieramy w nim projekt konsolowy, który zostawiamy bez zmian i od razu tworzymy projekt biblioteki (Class Library), który nazywamy np. „Domain” – co będzie oznaczać, że przechowywujemy tam pliki związane z odwzorowaniem danych w naszych tabelach, w środku tego projektu tworzymy też folder „Models”. Wiele tutoriali pomija ten krok, prosząc po prostu o utworzenie katalogu „Models” w projekcie głównym, jednak nasz poradnik zahaczy o więcej sposobów wykorzystania tej biblioteki, niż tylko aplikacje konsolowe, dlatego też warto mieć modele pod ręką, niezależne od reszty aplikacji.

Tak więc koniec już gadania – czas wziąć się do pracy 🙂

Całość rozpoczynamy od dodania do naszego projektu referencji do Entity Framework. Klikamy prawym przyciskiem myszy na projekt i wybieramy „Manage NuGet Packages”

Tak wyglądają nasze referencje po zainstalowaniu Entity Framework.

Tworzymy naszą klasę i dodajemy ją do folderu Models. W naszym przypadku będzie to klasa o nazwie „Album” – reprezentująca pojedynczą płytę.

Reprezentacja tabeli z bazy danych w postaci klasy zapisanej w języku C#.

Kolejnym krokiem jest stworzenie kontekstu bazy – kolejnej klasy, w której będziemy tworzyć obiekty odpowiadające fizycznej bazie danych, oraz które będą odpowiadać za komunikację z serwerem SQL.

Nazwijmy tą klasę „ApplicationDbContext”, oraz dodajmy w niej dziedziczenie po klasie „DbContext”. Następnie tworzymy konstruktor klasy, zawierający w sobie właściwość z klasy bazowej.

public class ApplicationDbContext : DbContext
{
public ApplicationDbContext()
: base("DefaultConnection")
{
Database.Initialize(false);
}

public DbSet<Album> Albums { get; set; }
}

Listing 1. Klasa ApplicationDbContext

Teraz kilka słów wyjaśnienia. Parametr base w konstruktorze klasy pozwala na nazwanie połączenia z bazą danych – jest więc ona dowolna. Funkcja, którą tu wywołujemy realizuje za nas tworzenie oraz zarządzanie plikiem bazy danych, który będzie zapisywał się w lokalnej instancji MS SQL.
Obiekty typu DbSet<T> reprezentują nasze tabele w bazie, z których będziemy mogli wydobywać informacje oraz je tam dodawać.

Kolejnym krokiem, już ostatnim przed rozpoczęciem pracy na bazie jest włączenie migracji. Uruchamiamy więc konsolę (w prawym górnym rogu, w polu Quick Launch wpisujemy „Console” i wciskamy ENTER).

Po otwarciu konsoli, wybieramy nasz projekt bazy z listy i wpisujemy „Enable-Migrations”, pierwsza migracja zostanie utworzona za nas i będzie zawierać w sobie tabelę, którą stworzyliśmy na potrzeby tego poradnika. Potem pozostaje wpisać tylko „Update-Database”, aby upewnić się, że wszystkie dane zostaną zapisane.

Jeżeli będziemy chcieli dodać kolejne tabele, wystarczy powtórzyć poprzednie kroki, dodając obiekt DbSet<T> w kontekście bazy, dodać migrację komendą „Add-Migration <nazwa>” i zaktualizować zmiany.

 

// Tutaj może być podział i reszta punktów z 2. Może iść do kolejnej części.

 

 

 

Ok, mam już bazę, co mogę z nią zrobić?

2.2 Tworzenie prostego programu dodającego i odczytującego dane z bazy danych.

 

Kolejnym zadaniem stojącym przed nami będzie utworzenie nowego, dowolnego projektu, ja tutaj używam aplikacji konsolowej, ale jeżeli czujesz się na siłach, możesz utworzyć aplikację okienkową i w niej testować bazę.

Po utworzeniu projektu, należy dodać referencję do naszych modeli. [WAŻNE! Trzymanie projektów w jednej solucji zdecydowanie ułatwia korzystanie z referencji]. Wybieramy więc referencje i dodajemy nasz projekt.

W głównej metodzie programu dodajemy obiekt naszej bazy, czyli „ApplicationDbContext”, wygląda to tak:

ApplicationDbContext db = new ApplicationDbContext();
Listing 2. Tworzenie obiektu bazy w programie

Po utworzeniu, możemy już cieszyć się dostępem do bazy. W prosty sposób możemy dodawać dane, za pomocą funkcji, które udostępnia nam Entity Framework. Poniżej przykład dodania nowego albumu do bazy:

private static void AddNewAlbum()
{
Album album = new Album //Tworzymy nowy obiekt
{
Artist = "Metallica",
Genre = "Thrash Metal",
Title = "Kill'em All",
Year = 1983
};
db.Albums.Add(album); //Dodaje obiekt do bazy
db.SaveChanges(); //Zapisuje zmiany
}

Listing 3. Kod dodający nowy obiekt do bazy danych.

Kiedy już mamy stworzony I dodany obiekt do bazy, możemy wczytać wszystkie nasze rekordy (czyli do tej pory jeden) do listy i je wyświetlić. Rozwiązanie pokazane poniżej jest trochę na wyrost (ponieważ mamy tylko jeden rekord), ale w przyszłości będzie przydatne, do wypisywania większej ilości rekordów.

private static void ShowAllAlbums()
{
foreach (var album in db.Albums.ToList()) //Wyświetla wszystkie albumy po kolei
{
System.Console.WriteLine("Wykonawca: {0}", album.Artist);
System.Console.WriteLine("Tytuł albumu: {0}", album.Title);
System.Console.WriteLine("Gatunek muzyki: {0}", album.Genre);
System.Console.WriteLine("Rok wydania: {0}", album.Year);
System.Console.WriteLine();
}
}

Listing 4. Funkcja wyświetlająca wszystkie albumy w oknie konsoli.

Po wykorzystaniu obu funkcji i skompilowaniu programu najprawdopodobniej ujrzycie coś podobnego do tego:

Teraz, po każdym wykonaniu programu, nasze dane będą się powielać, ponieważ baza zapisuje się i przy kolejnym uruchomieniu korzystamy z jej poprzedniej wersji. Możemy więc dowolnie modyfikować swoją funkcję dodawania i na końcu mieć dość sporą bazę naszych ulubionych płyt.

 

2.3 Edycja i usuwanie rekordów.

W tej części poradnika skupimy się na edycji i usuwaniu rekordów z bazy. Będzie to krótka część, jednak jak inaczej będziemy korzystać z bazy, bez możliwości edycji i usuwania rekordów?

Zacznijmy więc od edycji rekordów. Aby edytować dany wpis, musimy najpierw z bazy go wydostać – najłatwiej jest to zrobić za pomocą metody „FirstOrDefault”. Istnieją spory, czy nie lepiej używać „SingleOrDefault” – być może i druga opcja jest szybsza, ale jeżeli zadany przez warunek zwróci więcej niż jeden rekord, otrzymamy wyjątek, a tego nie chcemy.

Załóżmy, że chcemy zmienić gatunek muzyki w jednym z naszych albumów. Wydobywamy go z bazy, a potem bazując na tym obiekcie, modyfikujemy te właściwości, które chcemy zmienić. W naszym przypadku – właściwość „Genre”.

private static void EditAlbum()
{
Album album = db.Albums.FirstOrDefault(a => a.Title == "Kill'em All"); //Wyciągamy pierwszy napotkany album o tytule "Kill'em All"
if (album != null) //Sprawdzamy, czy album istnieje
{
album.Genre = "Inny rodzaj"; //Zmieniamy dane w obiekcie
db.Entry(album).State = EntityState.Modified; //Ustawiamy informację dla Entity Framework, że rekord został zmodyfikowany
db.SaveChanges(); //Zapisujemy zmiany.
}
}

Listing 5. Funkcja edytująca wpis w bazie danych

Jak widać – nie jest to takie trudne. Oczywiście ilość zmienionych właściwości zależy tylko i wyłącznie od nas.
Na razie nie martw się zapisem (a => a.Title == „Kill’em All”); zajmiemy się tym w dalszej części poradnika, gdzie wspomnę o LINQ.
Usuwanie jest bardzo podobne, również wyciągamy obiekt w taki sposób jak przy edycji, jednak później korzystamy z metody Remove, którą wywołujemy w taki sposób:

Album album = db.Albums.FirstOrDefault(a => a.Title == "Kill'em All"); //Wyciągamy pierwszy napotkany album o tytule "Kill'em All"
if (album != null) //Sprawdzamy, czy album istnieje
{
db.Albums.Remove(album); //Usuwamy z tabeli Albums wiersz
db.SaveChanges(); //Zapisujemy zmiany.
}

Listing 6. Sposób usuwania danych z tabeli.

 

3. Tworzenie relacji w Entity Framework Code First

W tej części poradnika zajmiemy się tworzeniem relacji znanych nam z baz danych, z wykorzystaniem Entity Framework. Do stworzenia tych połączeń, będziemy potrzebować dodatkowych klas (czyli tabel w naszej bazie). Poniżej zamieszczę kod klas, które ja użyłem, jednak oczywiście możecie użyć dowolnej innej, implementować je już umiecie :). Nie zapomnijcie po wszystkim wykonać migracji.

public class Song
{
public int Id { get; set; }
public int AlbumId { get; set; }
public string Title { get; set; }
public virtual Album Album { get; set; }
}

Listing 7. Klasa reprezentująca piosenkę w bazie danych

Zacznijmy od relacji „jeden do wielu” – będzie najprostsza do implementacji w naszym przykładzie.

Aby zrealizować ten przykład – dodałem nową klasę oraz musiałem zmodyfikować naszą klasę Album, dodając nowe pole:

public virtual ICollection<Song> Songs { get; set; }
Listing 8. Dodatkowa właściwość w klasie Album.cs

 

Ale po co, dlaczego, skąd „virtual” ?

Już tłumaczę. O ile sama struktura podstawowa tabeli „Song” wydaje się dość oczywista (klucz główny, klucz obcy i właściwość), to już samo powiązanie z obiektem klasy Album nie do końca musi być jasne i łatwe do domyślenia się. Otóż – Entity Framework samo ładuje referencje przy ładowaniu obiektu, jednak tylko wtedy, gdy prosimy o ten właśnie obiekt, nazywa się to „Lazy-Loading” i dzięki użyciu słowa kluczowego virtual możemy z tej technologii skorzystać.
Dodana za to kolekcja w klasie Album.cs pozwala nam dostać się do każdej z piosenek w albumie oddzielnie.
Mam nadzieję, że jest to w miarę jasne. Teraz trochę praktyki.

 

 

3.1 Korzystanie z relacji „jeden do wielu”

Przykład, który tutaj wykorzystam będzie bardzo prosty – do danego albumu, dodamy kilka utworów, a potem sobie je wyświetlimy.

private static void AddSongToAlbum()
{
Album album = db.Albums.FirstOrDefault(a => a.Title == "Kill'em All"); //Wyciągamy pierwszy napotkany album o tytule "Kill'em All"
if (album != null) //Sprawdzamy, czy album istnieje
{
album.Songs = new List<Song>(); // Tworzymy nową listę utworów w albumie
Song song = new Song
{
Album = album, // Referencja do obiektu albumu
Title = "The Four Horsemen"
};
album.Songs.Add(song); // Dodajemy nasz utwór do listy
db.Entry(album).State = EntityState.Modified; //Ustawiamy informację dla Entity Framework, że rekord został zmodyfikowany
db.SaveChanges(); //Zapisujemy zmiany.
}
}

Listing 9. Funkcja dodająca utwór do Albumu.

Teraz szybka edycja funkcji wyświetlającej:

private static void ShowAllAlbums()
{
foreach (var album in db.Albums.ToList()) //Wyświetla wszystkie albumy po kolei
{
System.Console.WriteLine("Wykonawca: {0}", album.Artist);
System.Console.WriteLine("Tytuł albumu: {0}", album.Title);
System.Console.WriteLine("Gatunek muzyki: {0}", album.Genre);
System.Console.WriteLine("Rok wydania: {0}", album.Year);
System.Console.WriteLine("Utwory:");
foreach (var song in album.Songs) // Wybieramy tylko utwory przypisane do danego albumu
{
System.Console.WriteLine("\t {0}", song.Title);
}
System.Console.WriteLine();
}
}

Listing 10. Funkcja wyświetlająca utwory wraz z albumami.

Po wszystkich zmianach powinniśmy ujrzeć na naszym ekranie podobny do tego efekt:

W ten sposób właśnie utworzyliśmy relację „jeden do wielu” – jeden album, posiadający wiele różnych utworów.

3.2 Korzystanie z relacji „wiele do wielu”

W tradycyjnych bazach danych relacja wiele do wielu lubi być problematyczna, ponieważ musimy ręcznie tworzyć tabele łączące i dbać o dodawanie tam wpisów z ID obu tabel. Na szczęście Entity Framework potrafi zrobić to za nas i robi to na tyle dobrze, że jeżeli nie ma dodatkowych wymagań, to możemy bez problemu dać mu możliwość zajęcia się tym zagadnieniem.

Tutaj po raz kolejny będziemy musieli wykonać kilka zmian w naszych klasach, jednak doświadczeni już poprzednimi zmianami, myślę, że nie będziecie mieć z tym problemu.

public class Author
{
public int Id { get; set; }
public int SongId { get; set; }
public string Name { get; set; }
public virtual ICollection<Song> Songs { get; set; }
}

Listing 11. Klasa odwzorowująca autora utworu

Powyższa klasa posłuży nam do przypisania danemu autorowi utworów, przy których tworzeniu brał udział. Za to do klasy „Song” dodajemy obiekt:

public virtual ICollection<Author> Authors { get; set; }

Tutaj dodajemy możliwość napisania utworu przez wiele autorów (muzyka, tekst). Tym samym stworzyliśmy relację wiele do wielu. Teraz pozostaje tylko dodać odpowiednie wpisy do kontekstu i dodać migrację.

Jeżeli przyjrzymy się utworzonej przez Entity Framework migracji zauważymy, że sam utworzył tabelę „dbo.AuthorSongs” – czyli naszą tabelę łączącą, o której wspominałem na początku.

 

 

Teraz przykład:

private static void AddAuthorsToSong()
{
Album album = db.Albums.FirstOrDefault(a => a.Title == "Kill'em All"); //Wyciągamy pierwszy napotkany album o tytule "Kill'em All"
if (album != null) //Sprawdzamy, czy album istnieje
{
List<Song> songs = album.Songs.ToList(); // Wyciągamy wszystkie utwory z albumu
if (songs.Count > 0) // Sprawdzamy, czy są przypisane utwory
{
Author author1 = new Author
{
Name = "James Hetfield",
Songs = songs
};

Author author2 = new Author
{
Name = "Lars Ulrich",
Songs = songs
};

foreach (var song in songs)
{
song.Authors = new List<Author> {author1, author2}; // Dodajemy autorów do każdego utworu po kolei
db.Entry(song).State = EntityState.Modified; // Ustawiamy informację dla Entity Framework, że rekord został zmodyfikowany
}

db.SaveChanges(); // Zapisujemy zmiany.
}
}
}

Listing 12. Funkcja dodająca autorów do utworów.

Teraz pozostaje nam jeszcze zmodyfikować odpowiednio funkcję wyświetlającą, aby obok nazw utworów otrzymać autorów danego utworu. Modyfikacja jest prosta, jednak używamy w niej też elementów LINQ.

System.Console.WriteLine("\t {0} ({1})",
song.Title, string.Join(", ", song.Authors.Select(s => s.Name)));

Listing 13. Zmiana w funkcji wyświetlającej.

 

Jeżeli wszystko poszło jak należy, powinniśmy zobaczyć na ekranie coś podobnego do tego:

3.3 Korzystanie z relacji „jeden do jednego”

W naszej aplikacji ciężko znaleźć sensowny sposób wykorzystania relacji jeden do jednego, ale zrobimy go na przykładzie dodania obrazu okładki płyty (a raczej obiektu, który będzie przechowywał informacje na jego temat).

public class Cover
{
public int Id { get; set; }
public int AlbumId { get; set; }
public string FilePath { get; set; }
public double Width { get; set; }
public double Height { get; set; }

public Album Album { get; set; }
}

Listing 14. Kod klasy przechowywującej informacje o pliku.

Dodany powyżej obiekt łączymy również relacją z poziomu Albumu i otrzymujemy w miarę kompletne informacje o pojedynczym albumie (tytuły, utwory, autorzy, okładka).

Praktycznego działania tej klasy nie do końca zaprezentujemy w okienku konsoli, ale jeżeli czujesz się na siłach, to spokojnie możesz przetestować sobie to w aplikacji okienkowej, czy to w WinForms, czy WPF.