Entity Framework Code First

Entity Framework to biblioteka oferująca dwa podejścia do kodowania procedur dostępu baz danych – Code First oraz Database First. W tym wpisie zajmiemy się tym pierwszym zagadnieniem. Zanim zacznę od praktycznej prezentacji przykładów, przybliżę Ci krótko na czym polega owa technika.

W metodyce Code First to kod naszego programu decyduje o tym jak będzie wyglądać nasza baza danych. Cały projekt tabel i kolumn przygotowujemy z poziomu naszego programu. Po stronie bazy, interesuje nas tylko i wyłącznie nazwa oraz parametry połączenia. Entity Framework resztę wykona za nas. Przygotuje odpowiednie zapytanie, zbuduje strukturę tabel, połączy je w relacje. Naszym pierwszorzędnym zadaniem jest utworzenie podstawowego modelu. Modelu, czyli zestawu klas, które odwzorowują tabele i kolumny w naszej bazie danych. Do nich będziemy się odwoływać i zamieniać na obiekty odpowiadające pojedynczemu wierszowi. W przypadku aktualizacji, usuwania czy wstawiania danych, Entity Framework automatycznie zamieni te obiekty na odpowiednie zapytania. Podejście Code First praktycznie w całości pozwala nam zrezygnować z własnoręcznego pisania w SQL’u. Nie jest to jednak zaleta, ponieważ pod względem optymalizacyjnym można uzyskać negatywne efekty. Metodyka ma za zadanie przyspieszyć projektowanie bazy danych, ale kosztem jej wydajności. W większości może się nadać tylko do mało złożonych aplikacji.

Pobierz Entity Framework z NuGet Gallery

Projekt modelu relacyjnego dla Entity Framework Code First

Po wstępie teoretycznym przejdźmy do przedstawienia projektu bazy danych. Żeby utworzyć model trzeba już znać przynajmniej podstawową strukturę danych jakie będą przechowywane. Mimo podejścia Code First, warto mieć tutaj model relacyjny. Musi być odpowiednio opisany. Zrozumiały przez bazę danych i Entity Framework. Definiujemy tutaj zarówno pola w naszej klasie, ale i również możemy zdefiniować ich odpowiedniki w bazie danych. W przeciwnym wypadku zostaną tutaj wstawione wartości domyślne, które pod kątem optymalizacji niekoniecznie są dobry podejściem. Mój projekt modelu wygląda następująco:

Model testowej bazy danych dla Entity Framework

Baza danych nie jest rozbudowana, ale to dlatego, że za cel postawiłem sobie prezentację możliwości Entity Framework w tym podejściu. Chcę Ci pokazać na prawdziwym przykładzie do jakiej struktury będziemy budować model. Jak widać mamy tutaj cztery tabele – użytkownicy, komputery, oprogramowanie i instalacje na poszczególnych stanowiskach. Cel? Przechowywanie informacji na temat tego jaki komputer ma dany użytkownik, a komputer jakie ma oprogramowanie. Czwarta tabela to tabela relacyjna, która będzie nam zwracać instalacje oprogramowania na poszczególnych stanowiskach i odwrotnie. Przejdźmy, więc do budowy modelu.

Tworzenie modelu

W naszym projekcie stwórzmy sobie katalog Model. Jest to najczęściej stosowana praktyka, która już po nazwie sugeruje nam z czym mamy do czynienia.

Okno rozwiązania projektu Entity Framework w Visual Studio

W modelu utwórzmy pierwszą klasę o nazwie User.

Okno dodawania nowej klasy dla Entity Framework

Klasę zawszę nazywam w liczbie pojedynczej, ponieważ odzwierciedla ona pojedynczy rekord w bazie danych. Musimy ją zrobić publiczną, aby mieć do niej dostęp na zewnątrz. Teraz odzwierciedlmy wszystkie kolumny dodając odpowiednie pola. Całość będzie wyglądać tak.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EntityFrameworkConsoleApp.Model
{
    public class User
    {
        public int UserId { get; set; }

        public string Forename { get; set; }

        public string Surname { get; set; }

        public string Login { get; set; }

        public string Password { get; set; }

        public string Email { get; set; }

        public DateTime Created { get; set; }

        public bool Active { get; set; }
    }
}

Większość naszych kolumn to ciągi znakowe, ale w bazie danych musimy je jeszcze dokładnie opisać. Klucz główny, definiujemy jako int, datę utworzenia – DateTime, a kolumny oparte na zero lub jeden typu bool. Entity Framework już na tym etapie pozwoliłby przekształcić tą klasę na odpowiednik w tabeli, ale my jeszcze zastosujemy tutaj odpowiednie atrybuty. Najpierw musimy dodać odpowiednie odwołania do bibliotek. Zamieńmy więc początek kodu na:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

Pozostałe odwołania nie będą nam do niczego potrzebne. Przynajmniej na etapie kiedy nie chcemy dodawać funkcji rozszerzających naszą klasę. Na razie sprowadzamy wszystko do czystego modelu. Kiedy dodamy odwołania możemy opisywać pola za pomocą odpowiednich atrybutów. W nich zawrzemy informację jak nazywa się tabela i poszczególne kolumny, które to klucz podstawowy, maksymalna długość ciągu znakowego czy typ danych w bazie danych. Oto finalny model tabeli Users z już opisanymi polami.

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EntityFrameworkConsoleApp.Model
{
    [Table("Users")]
    public class User
    {
        [Column("UserId", TypeName = "INT")]
        [Key]
        public int UserId { get; set; }

        [Column("Forename", TypeName = "VARCHAR")]
        [MaxLength(42)]
        public string Forename { get; set; }

        [Column("Surname", TypeName = "VARCHAR")]
        [MaxLength(42)]
        public string Surname { get; set; }

        [Column("Login", TypeName = "VARCHAR")]
        [MaxLength(16)]
        public string Login { get; set; }

        [Column("PasswordHash", TypeName = "CHAR")]
        public string Password { get; set; }

        [Column("Email", TypeName = "VARCHAR")]
        [MaxLength(128)]
        public string Email { get; set; }

        [Column("Created", TypeName = "DATETIME")]
        public DateTime Created { get; set; }

        [Column("Active", TypeName = "BIT")]
        public bool Active { get; set; }
    }
}

Opisaliśmy teraz tabelę, która będzie nazywać się Users. Nadaliśmy nazwy kolumn, które mogą być różne niż nazwa pola, zdefiniowaliśmy typ danych oraz długość. Atrybutem [Key] oznaczamy kolumnę jako klucz podstawowy. W takiej formie mamy gotową naszą pierwszą klasę modelu. Zabierzmy się teraz za pozostałe.

Pozostałe obiekty Entity Framework

Zaczynamy od klasy Computer.

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EntityFrameworkConsoleApp.Model
{
    [Table("Computers")]
    public class Computer
    {
        [Column("ComputerId", TypeName = "INT")]
        [Key]
        public int ComputerId { get; set; }

        public int? UserId { get; set; }

        [ForeignKey("UserId")]
        public User User { get; set; }

        [Column("Name", TypeName = "VARCHAR")]
        [MaxLength(42)]
        [Required]
        public string Name { get; set; }

        [Column("Model", TypeName = "VARCHAR")]
        [MaxLength(42)]
        public string Model { get; set; }

        [Column("IPAddress", TypeName = "VARCHAR")]
        [MaxLength(16)]
        public string IPAddress { get; set; }

        [Column("LoggedDate", TypeName = "DATETIME")]
        public DateTime? LoggedDate { get; set; }

        [Column("Active", TypeName = "BIT")]
        public bool Active { get; set; }
    }
}

Do każdego komputera może być zainstalowane odpowiednie oprogramowanie. Za nie będzie odpowiadać klasa Software.

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EntityFrameworkConsoleApp.Model
{
    [Table("Computers")]
    public class Computer
    {
        [Column("ComputerId", TypeName = "INT")]
        [Key]
        public int ComputerId { get; set; }

        public int? UserId { get; set; }

        [ForeignKey("UserId")]
        public User User { get; set; }

        [Column("Name", TypeName = "VARCHAR")]
        [MaxLength(42)]
        [Required]
        public string Name { get; set; }

        [Column("Model", TypeName = "VARCHAR")]
        [MaxLength(42)]
        public string Model { get; set; }

        [Column("IPAddress", TypeName = "VARCHAR")]
        [MaxLength(16)]
        public string IPAddress { get; set; }

        [Column("LoggedDate", TypeName = "DATETIME")]
        public DateTime? LoggedDate { get; set; }

        [Column("Active", TypeName = "BIT")]
        public bool Active { get; set; }
    }
}

Jako, że jeden wiele komputerów może mieć zainstalowane wiele programów potrzebna nam jest tabela relacyjna, która to wszystko powiąże. Będzie za to odpowiadać klasa Installation.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EntityFrameworkConsoleApp.Model
{
    [Table("Installations")]
    public class Installation
    {
        [Column("ComputerId", TypeName = "INT")]
        [Key]
        public int InstallationId { get; set; }

        public int ComputerId { get; set; }

        [ForeignKey("ComputerId")]
        public Computer Computer { get; set; }

        public int SoftwareId { get; set; }

        [ForeignKey("SoftwareId")]
        public Software Software { get; set; }
    }
}

Pojawiły się nowe rzeczy takie jak [ForeignKey]. Jest to nic innego jak klucz obcy. W parametrze wpisujemy jego nazwę lub nazwę kolumny, której odpowiada w danej tabeli. Atrybut zawsze przypisujemy do pola, który zwraca cały obiekt z tabeli relacyjnej. W ten sposób uzyskamy na przykład komputer, który należy do danego użytkownika. [Required] oznacza pole, które jest obowiązkowe, czyli musi zostać wypełnione. W bazie danych jest to odpowiednik NOT NULL. Ale już na etapie kodu Entity Framework będzie nas pilnował, aby zapytanie nie poszło z niewypełnionym polem.

Kolekcje z relacji

Mamy dodane relacje. Tabele nadrzędne będą zwracać obiekty z tabeli podrzędnych, ale tylko w jedną stronę. W przypadku na przykład użytkownika, który może mieć wiele komputerów nie jesteśmy w stanie na tym etapie wyciągnąć wszystkiego w jednym obiekcie. Musimy zatem dodać jeszcze jedno pole.

public virtual ICollection Computers { get; set; }

To pole zostanie uzupełnione obiektami klasy Computer, które należą do danego użytkownika. Kolekcję możemy przekonwertować do postaci listy lub nawet tablicy. Analogicznie dodajemy do pozostałych klas następujące pole.

public virtual ICollection Installations { get; set; }

Dlaczego i gdzie? Spójrz na model. Pojedynczy komputer może mieć wiele instalacji oprogramowania. Tak samo oprogramowanie może mieć wiele instalacji na różnych komputerach. Całość układa nam się więc w logiczną całość. Kiedy odwołamy się do obiektu komputera lub oprogramowania to zwróci nam dodatkowo wszystkie instalacje. Z kolei sama instalacja zwróci nam komputer i oprogramowanie, którego dotyczy. W ten oto sposób zbudowaliśmy model naszej bazy. Pora przejść do kontekstu.

Tworzenie kontekstu Entity Framework

Kontekst to oczywiście klasa, która definiuje obiekt uzyskujący dostęp do bazy danych. Zwraca nam obiekty z tabel, tworzy ją i zawiera informacje o bazie. Możemy tworzyć wiele różnych kontekstów, który każdy może być odpowiedzialny całkowicie za inną bazą danych lub zestaw tabel. Wiąże się to z dobrymi praktykami. Ale dzisiaj nie o tym. Musimy utworzyć pierwszy kontekst, który pozwoli nam na utworzenie bazy i dostęp do danych.

using EntityFrameworkConsoleApp.Model;
using System.Data.Entity;

namespace EntityFrameworkConsoleApp
{
    public class BuilderContext : DbContext
    {
        public BuilderContext(string connectionString) : base(connectionString) { }

        public virtual DbSet Users { get; set; }
        public virtual DbSet Computers { get; set; }
        public virtual DbSet Softwares { get; set; }
        public virtual DbSet Installations { get; set; }
    }
}

Klasa BuilderContext to podstawowy obiekt kontekstu, który będzie odpowiadać za dostęp do wszystkich danych oraz tworzyć bazę na wypadek, gdyby nie istniała. Struktura nie jest skomplikowana. Składa się z konstruktora z parametrem. W parametrze przekazujemy ciąg połączenia do bazy danych, oraz pól typu DbSet, gdzie T to nazwa klasy naszego obiektu modelu. Pola DbSet określają tabele w bazie danych jakie będą obsługiwane z poziomu tego kontekstu. Dzięki temu będziemy w stanie uzyskać dostęp do tych danych. Cała klasa dziedziczy po klasie DbContext biblioteki Entity Framework. Dodajmy jeszcze do naszego konstruktora funkcję, która będzie tworzyć bazę danych jeśli nie będzie istniała.

if (!this.Database.Exists())
    this.Database.Create();

Można by to jeszcze zrobić tak.

this.Database.CreateIfNotExists();

Obydwie metody są oczywiście dobre. Każda z nich sprawdza czy baza danych istnieje. Jeśli nie tworzy nową wraz z strukturą pobraną z naszego modelu. Sprawdźmy czy tak faktycznie jest.

static void Main(string[] args)
{
    BuilderContext builderContext = new BuilderContext("Server=192.168.5.5;Database=testowa;User Id=deweloper;Password=haslo;");
    if (builderContext.Database.Exists())
       Console.WriteLine("Baza danych istnieje");
    else
       Console.WriteLine("Baza danych nie istnieje");
    Console.ReadKey();
}

Przy pierwszym wywołaniu metody możemy zobaczyć, że baza nie istnieje. Dlatego, że została dopiero utworzona. Dopiero drugie uruchomienie projektu zwróci nam zamierzony efekt.

Okno konsoli sprawdzania istnienia bazy danych poprzez Entity Framework

Najlepiej jest to sprawdzić łącząc się do serwera za pomocą Management Studio. Zobaczymy, że baza o podanej, w ciągu połączenia, nazwie została utworzona, a w niej struktura taka jak w modelu.

Utworzona baza danych w SQL Management Studio

A tutaj model wygenerowany przez Management Studio.

Model wygenerowany w SQL Management Studio

Na przykładzie klasy Computer możemy zobaczyć jak zostały odzwierciedlone wszystkie kolumny. Wszystko zgodnie z oczekiwaniami.

Dla ciekawskich. Tabela o nazwie MigrationHistory jest tworzona automatycznie przez Entity Framework i zawiera informacje o wersjach bazy. W przypadku kiedy zachodzą zmiany w modelu można je zesynchronizować.

Podstawowe operacje na danych

Wiemy już jak zaprojektować model i utworzyć z tego bazę danych wraz z pełną strukturą. Warto teraz coś w tej bazie zapisać. Przejdźmy teraz do podstawowych operacji wstawiania, aktualizowania i usuwania danych. Najpierw zacznijmy od pierwszej. Czyli wstawiania danych. Mamy pustą tabelę Users. Spróbujmy wygenerować jakiś użytkowników. Tworzymy nową metodę w naszej aplikacji konsolowej, która będzie nam zwracała listę użytkowników.

static List GetUsers()
        {
            List Users = new List();

            Users.Add(new User() { Forename = "Mateusz", Surname = "Dziedzic", Login = "m.dziedzic", Password = "asddadadsa", Email = "Mateusz.Dziedzic@mdatelier.pl", Created = DateTime.Now, Active = true });
            Users.Add(new User() { Forename = "Mateusz", Surname = "Kaczor", Login = "m.Kaczor", Password = "asddadadsa", Email = "mateusz.kaczor@mdatelier.pl", Created = DateTime.Now, Active = true });
            Users.Add(new User() { Forename = "Aleksander", Surname = "Wójcik", Login = "a.wojcik", Password = "asddadadsa", Email = "aleksander.wojcik@mdatelier.pl", Created = DateTime.Now, Active = true });
            Users.Add(new User() { Forename = "Zuzanna", Surname = "Domańska", Login = "z.domanska", Password = "asddadadsa", Email = "zuzanna.domanska@mdatelier.pl", Created = DateTime.Now, Active = true });
            Users.Add(new User() { Forename = "Kornel", Surname = "Piotrowski", Login = "k.piotrowski", Password = "asddadadsa", Email = "kornel.piotrowski@mdatelier.pl", Created = DateTime.Now, Active = true });

            return Users;
        }

Mamy listę teraz musimy ją zapisać w bazie danych. W głównej metodzie robimy to tak.

static void Main(string[] args)
        {
            BuilderContext builderContext = new BuilderContext("Server=192.168.5.5;Database=testowa;User Id=deweloper;Password=haslo;");

            builderContext.Users.AddRange(GetUsers());
            builderContext.SaveChanges();

            Console.ReadKey();
        }

Lub można jeszcze każdy obiekt osobno, ale nie jest to zalecane. Najlepiej wszystko załatwić jednym zapytaniem. Sprawdźmy czy to działa.

Po odpaleniu programu konsola nie zwróciła błędów. Czyli jest dobrze. Żeby zobaczyć cały efekt wchodzimy w SQL Management i wyświetlamy dane z tabeli Users. Jak widzimy dodawanie nowych wierszy jest bardzo proste. Tworzymy nowe obiekty klasy użytkownik. Następnie dodajemy pojedynczy obiekt lub listę do kolekcji obiektów Users klasy kontekstu. Na końcu zapisujemy zmiany metodą SaveChanges(). Przy zapisywaniu następuje generowanie zapytania i wysłanie do SQL Server. Tutaj prawdopodobnie najwięcej błędów będziesz przechwytywać. Dlatego warto dodać go do klauzuli try catch. Aktualizacja danych wygląda podobnie z tym, że tutaj musimy najpierw pobrać obiekt, zmienić, a potem zapisać.

static void Main(string[] args)
        {
            BuilderContext builderContext = new BuilderContext("Server=192.168.5.5;Database=testowa;User Id=deweloper;Password=haslo;");

            var user = builderContext.Users.Single(x => x.UserId == 1);
            user.Forename = "Tomek";
            builderContext.SaveChanges();

            Console.ReadKey();
        }

Usuwanie polega na skorzystaniu z metody Remove(), która usuwa dany obiekt z kolekcji, a następnie zapisuje zmiany. Przy zapisie generowane jest zapytanie SQL DELETE usuwający dany wiersz.

static void Main(string[] args)
        {
            BuilderContext builderContext = new BuilderContext("Server=192.168.5.5;Database=testowa;User Id=deweloper;Password=haslo;");

            builderContext.Users.Remove(builderContext.Users.Single(x => x.UserId == 1));
            builderContext.SaveChanges(); 

            Console.ReadKey();
        }

W kwestii operacji na danych nie ma tutaj dużej filozofii. Trzeba tylko zwracać uwagę na to czy wszystkie pola naszej klasy się zgadzają. Zgodnie z tym co opisaliśmy w modelu. W przeciwnym razie program przy operacji zapisu zacznie sypać wyjątkami. I słusznie, bo w ten sposób pilnuje poprawności wprowadzanych danych.

Podsumowanie

W tym wpisie pokazałem podstawy programowania z biblioteką Entity Framework w połączeniu z podejściem Code First. Począwszy od stworzenia modelu, a skończywszy na podstawowych operacjach. Jeśli uważnie prześledziłeś wszystkie wyniki działania biblioteki Entity Framework to na pewno zauważysz wiele kompromisów w tym podejściu. Między innymi brak kontroli nad tym co tak naprawdę jest tworzone po stronie bazy danych. Mimo, że wiele opisujemy to nie zawsze może zostać to zinterpretowane po naszej myśli. Z drugiej strony taka prosta aplikacja to kwestia kilku minut. Tak naprawdę mamy cały algorytm dostępu do danych. Dlatego podejście Code First nie jest zalecane dla dużych projektów, gdzie liczy się wydajność. Możemy oczywiście połączyć ręczne modelowanie w odtwarzanie potem w postaci klas, ale tutaj zdecydowanie lepszym podejściem okaże się Database First, które poświęcony jest już osobny wpis.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *