Używanie PostgreSQL w projekcie .NET – Entity Framework
Żadna bardziej zaawansowana aplikacja nie może obejść się bez mechanizmów służących do utrwalania danych. W celu realizacji tego zadania najczęściej sięga się po rozwiązania jakimi są bazy danych (zarówno SQL, jak i noSQL). Komunikacja z bazami relacyjnymi najczęściej realizowana jest przy użyciu języka SQL. Taki stan rzeczy sprawia, że podczas tworzenia oprogramowania sporą część czasu należy poświęcić na konstruowanie takich zapytań – a jak wiemy czas, to pieniądz. Na domiar złego różne systemy bazodanowe mimo implementacji standardu SQL nie są ze sobą w stu procentach kompatybilne. Co za tym idzie, najczęściej po wyborze jakiegoś konkretnego silnika bazodanowego na etapie projektowania aplikacji, zostaje on już na stałe z nią związany. Po prostu przerzucenie się na inne rozwiązanie, kiedy oprogramowanie jest już gotowe wiązałoby się z masą dodatkowej pracy, polegającej na modyfikacji wielu przygotowanych wcześniej zapytań. Da się, ale najczęściej nie ma na to czasu. Na szczęście istnieje rozwiązanie, które może zaradzić tym dwóm problemom. Można dzięki niemu zaoszczędzić zarówno czas podczas pisania aplikacji (dzięki rezygnacji z konstruowania zapytań SQL) oraz zapewnić w łatwy sposób kompatybilność między różnymi silnikami baz danych.
Czym jest ORM? Podejście SQL vs ORM
ORM czyli Object–relational mapping to sposób odwzorowania obiektowej reprezentacji danych używanej w większości współczesnych języków programowania na reprezentację relacyjną – używaną w popularnych silnikach baz danych. Brzmi dosyć prosto – zamieniamy obiekty na wpisy w tabelach, które są powiązane relacjami. Pisząc jakąkolwiek aplikację, która łączy się do bazy danych właśnie takie odwzorowanie ręcznie tworzymy. Najczęściej wygląda to tak, że jakaś funkcja wykonuje np. operację SELECT na tabeli X, a zwrócone dane lądują w obiekcie Y reprezentującym w kodzie programu zwrócone dane. Jak wspominałem wcześniej takie podejście jest dosyć czasochłonne. Z tego też powodu powstały narzędzia, które ułatwiają pracę z bazami danych z poziomu języków obiektowych. Takim narzędziem dla języka C# jest (oczywiście między innymi) Entity Framework. Dzięki niemu nie ma potrzeby ręcznego tworzenia zapytań SQL, a danymi można zarządzać tylko z poziomu kodu. Proces odczytu oraz zapisu danych z/do bazy jest realizowany „pod maską”.
Podejście Code First i Database First
Entity Framework pozwala podejść do zadania utworzenia bazy danych oraz powiązania jej z projektem na dwa sposoby. Pierwszym i w moim odczuciu lepszym jest Code-First. Polega on na tym, że tworzymy interesujące nas struktury danych w sposób obiektowy, a następnie generujemy w sposób automatyczny bazę danych ze wszelkimi relacjami. W dalszej części artykułu pokażę jak wygląda to w praktyce.
Drugim sposobem jest podejście Database-First. Polega on na tym, że najpierw tworzymy bazę danych, a następnie na jej podstawie generujemy obiektowy kod reprezentujący jej strukturę.
Efekt końcowy powinien być taki sam – powiązane ze sobą struktury danych w pamięci programu oraz bazie danych. Od tej pory każda zmiana wykonana na danych w pamięci programu potwierdzona przez wywołanie funkcji SaveChanges(); automatycznie będzie zapisywać zmiany także w bazie danych.
Jak używać Entity Framework? Instalacja wymaganych pakietów, utworzenie bazy danych
Żeby rozpocząć pracę z Entity należy na wstępnie przygotować sobie dostęp do jakiejś bazy danych. Entity Framework wspiera połączenia do większości popularnych silników baz danych (MS SQL, SQLite, PostgreSQL itd). Na potrzeby tego opracowania ja przygotowałem sobie czystą instalację PostgreSQL. W nowo utworzonym projekcie należy doinstalować poniższe pakiety. W moim przypadku jest to aplikacja konsolowa.
- Microsoft.EntityFrameworkCore
- Npgsql.EntityFrameworkCore.PostgreSQL
- Microsoft.EntityFrameworkCore.Tools
Kolejnym krokiem jest utworzenie nowej klasy. Ja nazwałem ją ApplicationDbContext, powinna ona dziedziczyć po klasie DbContext. W klasie należy dodać funkcję override OnConfiguring, w której ustawia się parametry połączenia z bazą:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | using Microsoft.EntityFrameworkCore; namespace EntityTest { public class ApplicationDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseNpgsql(@"Host=localhost;Port=5432;Database=TestDatabase;Username=postgres;Password=Haslo123!"); //connection string } } } } |
Następnie tworzymy jakiś model danych. Załóżmy, że będą to księgarnie. Z kolei w księgarniach znajdują się jakieś książki. Reprezentacja obiektowa będzie wyglądać mniej więcej w ten sposób:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | namespace EntityTest.Model { public class Book { public int Id { get; set; } public string Name { get; set; } public string Title { get; set; } public string Description { get; set; } public string Author { get; set; } public Book() { } public Book(string name, string title, string description, string author) { Name = name; Title = title; Description = description; Author = author; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | namespace EntityTest.Model { public class BookStore { public int Id { get; set; } public string Name { get; set; } public string Address { get; set; } public List<Book> Books { get; set; } public BookStore() { } public BookStore(string name, string address, List<Book> books) { Name = name; Address = address; Books = books; } } } |
To o czym należy pamiętać, to fakt że każda z takich klas powinna zawierać bezargumentowy konstruktor. W przeciwnym wypadku Entity Framework nie będzie w stanie odczytywać i zapisywać danych. Ważne jest także utworzenie właściwości typu int o nazwie Id – będzie to klucz główny w tabeli. Kiedy mamy gotowe klasy, można zabrać się za zdefiniowanie tabel. Te operację wykonuje się w klasie dziedziczącej po DbContext. Poniżej uzupełniony kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using EntityTest.Model; using Microsoft.EntityFrameworkCore; namespace EntityTest { public class ApplicationDbContext : DbContext { public DbSet<BookStore> BookStores { get; set; } //definicja tabeli public DbSet<Book> Books{ get; set; } //definicja tabeli protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseNpgsql(@"Host=localhost;Port=5432;Database=TestDatabase;Username=postgres;Password=Haslo123!"); } } } } |
Jeżeli wszystko do tej pory przebiegło zgodnie z planem, to program powinien już pomyślnie się kompilować. Możemy przejść do generowania bazy danych na podstawie modelu obiektowego, który przed chwilą utworzyliśmy. W tym celu w konsoli menadżera pakietów należy wpisać komendę Add-Migration migration_v1. Efektem wykonania tej komendy będzie plik *.cs, w którym zostanie wygenerowany kod odpowiadający za utworzenie nowej bazy danych. Następnie wywołujemy komendę Update-Database, która ten kod wykonuje i tworzy nową bazę danych.
I to… właściwie tyle. Od tej pory jeżeli wprowadzimy jakiekolwiek zmiany w modelu należy ponownie wykonać te dwie komendy, aby zaktualizować strukturę bazy danych. Zaglądając do narzędzia pgAdmin możemy potwierdzić, że baza została utworzona:
Jak można zauważyć w tabeli Book powstało jakieś tajemnicze pole BookStoreId, którego nie definiowaliśmy. Jest to nic innego jak klucz obcy z tabeli BookStores, który wiąże relacją te dwie tabele.
Dodawanie danych
Dane do bazy danych dodaje się w bardzo prosty sposób. W tym celu należy po prostu utworzyć odpowiednie obiekty, po czym dodać je do utworzonej instancji klasy ApplicationDbContext. Ważne żeby pamiętać o wykonaniu metody SaveChanges() – to ona inicjuje zapis zmian, które zaszły w obiektach do bazy danych.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | using EntityTest.Model; namespace EntityTest { internal class Program { public static ApplicationDbContext context = new ApplicationDbContext(); //utworzenie obiektu odpowiadającego za mapowanie danych do bazy static void Main(string[] args) { //utworzenie list książek dla dwóch ksiegarni List<Book> booksWarsaw = new List<Book>(); List<Book> booksCracow = new List<Book>(); for (int i=0; i<=10; i++) { Book book = new Book("Książka w Warszawie " + i, "Tytuł " + i, "Opis " + i, "Autor " + i); Book book2 = new Book("Książka w Krakowie " + i, "Tytuł " + i, "Opis " + i, "Autor " + i); booksWarsaw.Add(book); booksCracow.Add(book2); } //dodanie książek do księgarni BookStore BookStoreWarsaw = new BookStore("Księgarnia 1", "Warszawa", booksWarsaw); BookStore BookStoreCracow = new BookStore("Księgarnia 2", "Kraków", booksCracow); //dodanie danych do obiektu reprezentującego bazę context.BookStores.Add(BookStoreWarsaw); context.BookStores.Add(BookStoreCracow); //właściwy zapis do bazy context.SaveChanges(); } } } |
Po wykonaniu powyższego kodu do bazy trafią pierwsze wpisy:
Odczytywanie danych
Odczytywać dane można na dwa sposoby – przy użyciu zapytań LINQ lub w sposób standardowy iterując po kolekcji przy użyciu np. pętli foreach. Zalecam jednak używanie zapytań LINQ, bo są po prostu wydajniejsze. Kod wypisujący wszystkie książki z księgarni w Warszawie wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using EntityTest.Model; using Microsoft.EntityFrameworkCore; namespace EntityTest { internal class Program { public static ApplicationDbContext context = new ApplicationDbContext(); //utworzenie obiektu odpowiadającego za mapowanie danych do bazy danych static void Main(string[] args) { var bookStore = context.BookStores.Include(x => x.Books).OrderBy(y =>; y.Id).Where(z => z.Address == "Warszawa").First(); //używamy Include, ponieważ bez niego nie zostanie załadowana zagnieżdżona lista i otrzymamy NullReferenceException //przeglądamy wszystkie książki z księgarni foreach(var book in bookStore.Books) { Console.WriteLine(book.Id + "," + book.Name); } } } } |
W efekcie zostają wyświetlone wszystkie wybrane rekordy:
Usuwanie danych
Usuwanie danych również nie należy do operacji skomplikowanych. Sprowadza się do odpowiedniego wywołania funkcji Remove() na liście oraz zapisania zmian poprzez wywołanie SaveChanges(). Należy pamiętać, że cały czas operujemy na obiekcie context.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using EntityTest.Model; using Microsoft.EntityFrameworkCore; namespace EntityTest { internal class Program { public static ApplicationDbContext context = new ApplicationDbContext(); //utworzenie obiektu odpowiadającego za mapowanie danych do bazy danych static void Main(string[] args) { var bookStore = context.BookStores.Include(x => x.Books).OrderBy(y => y.Id).Where(z => z.Address == "Warszawa").First(); //wyszukanie księgarni z Warszawy var bookToRemove = context.Books.OrderBy(x => x.Id).Where(y => y.Id == 2).First(); //wyszukanie książki do usunięcia context.Books.Remove(bookToRemove); //usunięcie książki z bazy context.SaveChanges(); //zapisanie zmian foreach (var book in bookStore.Books) { Console.WriteLine(book.Id + "," + book.Name); } } } } |
W efekcie z bazy zniknęła książka od Id = 2.
Edycja danych
Edytowanie danych wygląda analogicznie do poprzednich przykładów:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using EntityTest.Model; using Microsoft.EntityFrameworkCore; namespace EntityTest { internal class Program { public static ApplicationDbContext context = new ApplicationDbContext(); //utworzenie obiektu odpowiadającego za mapowanie danych do bazy danych static void Main(string[] args) { var bookStore = context.BookStores.Include(x => x.Books).OrderBy(y => y.Id).Where(z => z.Address == "Warszawa").First(); //wyszukanie księgarni z Warszawy var bookToEdit = context.Books.OrderBy(x => x.Id).Where(y => y.Id == 3).First(); //wyszukanie książki do edycji bookToEdit.Author = "Nowy autor"; //edycja pola context.SaveChanges(); //zapisanie zmian foreach (var book in bookStore.Books) { Console.WriteLine(book.Id + "," + book.Name + "," + book.Author); } } } } |
I tutaj również dane w bazie zostały odpowiednio zmodyfikowane:
Podsumowanie
Z postaw to by było na tyle. Oczywiście ten wpis nie wyczerpuje tak obszernego tematu jakim jest Entity Framework, bo nie poruszyłem tutaj chociażby kwestii definiowania relacji. Myślę jednak, że tekst ten jest dobrym wprowadzeniem do tematu. Jeżeli ktoś ma jakieś pytania lub uwagi – zachęcam do komentowania.
1 comment