Wstęp i opis
Podczas laboratoriów wykonany zostanie projekt encji wraz z klasami bazowymi, oraz zaprezentowane zostanie wykonanie połączenia do bazy danych wraz z wstępną konfiguracją platformy mapowania obiektowo-relacyjnego EF Core.
Projekt Encji
Dodanie encji do projektu jako klasy danych
Mając zaprojektowane encje, możemy przejść do ich implementacji. Zacznijmy od stworzenia nowego podkatalogu Entities
w projekcie ApartmentRental.Infrastructure
. Posłuży on do przechowywania wspomnianych modeli. W katalogu zaczniemy od wyodrębnienia klasy bazowej BaseEntity
, która będzie dziedziczona przez wszystkie pozostałe encje. Klasa ta składać się będzie z trzech właściwości:
Id
- identyfikator - typuint
DateOfCreation
- data stworzenia - typuDateTime
DateOfUpdate
- data aktualizacja - typuDateTime
Klasa DateTime
, będzie typem naszych dwóch właściwości DateOfCreation
oraz DateOfUpdate.
Reprezentuje ona moment w czasie, wyrażony domyślnie jako data wraz z godziną, minutą i sekundą.
Aby stworzyć nową klasę w IDE JetBrains Rider, kliknij prawym przyciskiem myszy w folder Entities
oraz wybierz Add->Class/Interface
. Dodajmy teraz właściwości klasy. W C# domyślne pola do zapisywania oraz odczytywania danych w obiekcie wraz zachowaniem enkapsulacji (settery i gettery) mają uproszczoną składnie. Podczas dodawania nowych właściwości, podstawowe pola zapisu oraz odczytu dodajemy za pomocą dopisania wyrażenia { get; set;}
po prawej stronie od nazwy danej właściwości. Przy pomocy środowiska programistycznego całą operację można wykonać szybciej przy pomocy wpisania skrótu prop
, który wygeneruje nam odpowiednią linijkę, którą należy uzupełnić o wybrany typ właściwości oraz nazwę. Polecam również przeczytanie dokumentacji, która opisuje więcej przypadków oraz możliwości edycji pól odczytu oraz zapisu.
Do stworzonej klasy BaseEntity
oraz właściwości definiującej identyfikator encji, należy dodać jeszcze dwa atrybuty:
Key
- służąca do oznaczania jednej lub więcej właściwości encji, które zapewniają jej unikalną tożsamośćDatabaseGeneratedAttribute
- określający sposób, w jaki baza danych generuje wartości dla właściwości - wraz z opcjąDatabaseGeneratedOption.Identity
, przy pomocy której generowana jest nowa wartość identyfikatora, kiedy do bazy danych dodawany jest nowy wiersz
Gotowa klasa BaseEntity
powinna prezentować się następująco:
Zadanie 1
- Dodaj brakujące klasy encji w folderze Entities: a.
Address
dziedzicząca poBaseEntity
, o właściwościach: i. Ulica (string) ii. Numer mieszkania (nullable string) iii. Numer budynku (nullable string) iv. Miasto (string) v. Kod pocztowy (string) vi. Kraj (string) ii.Account
dziedzicząca poBaseEntity
, o właściwościach: i. Imię (string) ii. Nazwisko (string) iii. Email (string) iv. Numer telefonu (string) v. Flaga “Czy konto jest aktywne” (bool) iii.Apartment
dziedzicząca poBaseEntity
, o właściwościach: i. Wysokość czynszu (decimal) ii. Liczba pokoi (integer) iii. Wielkość mieszkania w metrach kwadratowych (integer) iv. Piętro (integer) v. Flaga „Czy jest winda” (bool) iv.Image
dziedzicząca poBaseEntity
, o właściwościach: i. Dane (tablica bajtów) v. Landlord dziedzicząca poBaseEntity
, o właściwościach: i. Apartamenty (lista klasy Apartment) vi.Tenant
dziedzicząca po klasieBaseEntity
, o właściwościach: i. Apartament (klasa Apartment)
NuGet
Niezbędnym narzędziem każdej nowoczesnej platformy programistycznej jest mechanizm, dzięki któremu programiści mogą tworzyć, udostępniać i wykorzystywać użyteczny kod. Często taki kod jest łączony w “pakiety”, które zawierają skompilowany kod (jako biblioteki DLL) wraz z inną zawartością potrzebną w projektach korzystających z tych pakietów. W przypadku .NET (w tym .NET Core) wspieranym przez Microsoft mechanizmem udostępniania kodu jest NuGet. Definiuje on w jaki sposób pakiety dla .NET są tworzone, hostowane i konsumowane. Pakiet NuGet to pojedynczy plik ZIP z rozszerzeniem .nupkg
który zawiera skompilowany kod (DLL), inne pliki związane z tym kodem oraz manifest zawierający takie informacje, jak np. numer wersji pakietu. Deweloperze, którzy posiadają kod którym chcą opublikować, tworzą pakiety i publikują je na (publicznym lub prywatnym) serwerze. Konsumenci pakietów otrzymują te pakiety z odpowiednich hostów, dodają je do swoich projektów, a następnie wywołują funkcjonalność pakietu w kodzie swojego projektu. NuGet sam zajmuje się wszystkimi pośrednimi szczegółami.
EF Core
Entity Framework (EF) Core jest lekką, rozszerzalną, otwarto-źródłową i wielo-platformową biblioteką dostępu do danych. EF Core służy jako mapper obiektowo-relacyjny (ORM), który:
- umożliwia deweloperom .NET pracę z bazą danych przy użyciu obiektów
- eliminuje konieczność pisania kodu dostępu do danych
- wspiera większość silników bazodanowych
Dodanie EF Core i Sqlite do projektu
Wykorzystując NuGet, dodamy pakiety potrzebne do generacji mapowania obiektowo- relacyjnego, połączenia do bazy danych oraz do tworzenia tzw. plików migracyjnych. Prezentowane rozwiązania podczas laboratoriów opierać się będzie na lokalnej bazie danych Sqlite. W celu dodania pakietów, należy wybrać zakładkę NuGet znajdującą się na dole środowiska programistycznego JetBrains Rider.
W zakładce powinna wyświetlić się wyszukiwarka z listą pakietów, które można dodać do stworzonych na poprzednich laboratoriach projektów (by dodać pakiet do projektu wystarczy kliknąć przycisk „plusika”).
Zadanie 2
- Wyszukaj pakiet Microsoft.EntityFrameworkCore w wersji 6.0.3 i dodaj go do projektów ApartmentRental.API oraz ApartmentRental.Infrastructure
- Wyszukaj pakiet Microsoft.EntityFrameworkCore.Design w wersji 6.0.3 i dodaj go do projektu ApartmentRental.API
- Wyszukaj pakiet Microsoft.EntityFrameworkCore.Sqlite w wersji 6.0.3 i dodaj go do projektów ApartmentRental.API oraz ApartmentRental.Infrastructure
Konfiguracja EF Core i implementacja mapowania obiektowo-relacyjnego
Kontekst
Na sam początek zaczniemy od stworzenia logiki, która będzie obsługiwało kontekst bazodanowy w aplikacji. W projekcie ApartmentRental.Infrastructure
należy stworzyć folder Context
a w nim klasę MainContext.
Utworzoną klasę należy rozszerzyć o klasę DbContext
z przestrzeni Microsoft.EntityFrameworkCore
oraz należy dodać pusty konstruktor oraz konstruktor przyjmujący klasę DbContextOptions
i przekazać parametr options do rodzica.
Klasa DbContext
jest połączeniem wzorców projektowych Repository oraz Unit of Work. Jej instancja reprezentuje sesję z bazą danych i jest używana do odpytywania oraz zapisywania instancji encji.
Connection string
Następnym krokiem który wykonamy jest skonfigurowania adresu do bazy danych. W tym celu musimy przeciążyć metodę OnConfiguring
z argumentem DbContextOptionsBuilder.
Po wcześniejszym zainstalowaniu pakietu Microsoft.EntityFrameworkCore.Sqlite
wystarczy użyć metody UseSqlite
ze wspomnianego powyżej argumentu i przekazać do niej connection string: DataSource=dbo.ApartmentRental.db
. Jako że Sqlite jest naszą lokalną bazą danych, ta konfiguracja pozwoli nam na automatyczne wygenerowanie bazy w dalszej części laboratorium.
DbSet
Kolejnym krokiem konfiguracji oraz implementacji jest dodanie do klasy właściwości DbSet
dla każdej utworzonej poprzednio encji. DbSet
reprezentuje zbiór wszystkich encji danego typu, które znajdują się w kontekście lub które mogą być odpytane w bazie danych, poprzez operacje CRUD (create, read, update, delete). Zaimplementowany DbSet
dla każdej enci prezentuje się następująco:
Powiązania pomiędzy encjami
Najczęstszym wzorcem relacji jest posiadanie właściwości nawigacyjnych zdefiniowanych na obu końcach relacji oraz stworzenie klucza obcego, zdefiniowanego w klasie encji zależnej. Jeśli para właściwości nawigacyjnych zostanie znaleziona pomiędzy dwoma typami, to zostaną one skonfigurowane jako odwrotne właściwości nawigacyjne tej samej relacji. Jeśli encja zależna zawiera właściwość o nazwie pasującej do jednego z tych wzorców, to zostanie ona skonfigurowana jako klucz obcy.
Całość została opisana w dokumentacji.
Przejdźmy teraz do implementacji powiązań pomiędzy encjami. Analizując projekt struktury bazodanowej, należy wysunąć poniższe powiązania:
- Tabela
Account
posiada jeden klucz obcyAddressId
do tabeliAddress
- Tabela
Image
posada jeden klucz obcyApartmentId
do tabeliApartment
- Tabela
Landlord
posiada jeden klucz obcyAccountId
do tabeliAccount
- Tabela
Tenant
posiada jeden klucz obcyAccountId
do tabeliAccount
- Tabela
Apartment
posiada trzy klucze obceAddressId
,LandlordId
orazTenantId
do tabelAddress
,Landlord
,Tenant
W tym celu, w każdej klasie trzeba dodać właściwość opisującą klucz obcy oraz obiekt. Dla wyżej wymienionych tabel, wszystkie właściwości klas po implementacji prezentują się następująco:
Ostatnim etapem konfiguracji relacji pomiędzy encjami, jest zdefiniowanie jak encje powinny się zachować podczas usuwania rekordów. Przyjmiemy że jeśli w sytuacji której będzie usuwany wpis o danym apartamencie, to wraz z nim zostaną usunięte wszystkie zdjęcia. Podobną strategie przyjmiemy podczas kasowania danych dla właściciela obiektu. Podczas kasowania rekordu zostaną usunięte wszystkie powiązane z nim apartamenty. Aby osiągnąć opisane wyżej założenia, należy przeciążyć metodę OnModelCreating,
która przyjmuje argument ModelBuilder.
W ciele metedy używając funkcji argumentu, zdefiniujmy powiązanie pomiędzy encjami oraz kaskadową metodę kasowania danych.
Proces migracji - wygenerowania mapowań obiektowo-relacyjnych oraz stworzenie lokalnej bazy danych
W rzeczywistych projektach modele danych zmieniają się w miarę wdrażania nowych funkcjonalności. Podczas dodawania/usuwania encji lub ich właściwości musimy pamiętać o zmienianiu równiach schematu bazy danych, tak aby zachować odpowiednią synchronizację. Funkcja migracji w EF Core umożliwia przyrostową aktualizację schematu bazy danych w celu utrzymania go w synchronizacji z modelem danych aplikacji przy jednoczesnym zachowaniu istniejących danych w bazie. W ogólnym zarysie migracje działają w następujący sposób:
- Gdy wprowadzana jest zmiana modelu danych, deweloper używa narzędzi EF Core, aby dodać odpowiednią migrację opisującą aktualizacje niezbędne do utrzymania synchronizacji schematu bazy danych. EF Core porównuje bieżący model z snapshotem starego modelu, aby określić różnice, po czym generuje pliki źródłowe migracji. Wygenerowane pliki można po operacji obejrzeć w kodzie źródłowym aplikacji.
- Po wygenerowaniu nowej migracji można ją zastosować do bazy danych na różne sposoby. EF Core zapisuje wszystkie zastosowane migracje w specjalnej tabeli historii, dzięki czemu wie, które migracje zostały zastosowane, a które nie.
Przed rozpoczęciem procesu migracji, wymagane jest zainstalowanie odpowiedniego rozszerzenia do .Net SDK. W konsoli/terminalu wpisz poniższa komendę:
1
dotnet tool install —global dotnet-ef
Następnie w konsoli przejdź do głównego katalogu solucji ApartmentRental.API i wykonaj komendę:
1
2
dotnet ef migrations add initial —project ApartmentRental.Infrastructure —startup-project
ApartmentRental.API
Powyższa komenda wygeneruje pliki migracji. Możemy je zobaczyć w folderze Migrations
w projekcie ApartmentRental.Infrastructure
. Mają one formę klas c#.
Wygenerowane pliki migracji posłużą teraz do stworzenia (a w późniejszym czasie do aktualizacji) bazy danych. W tym celu w folderze w którym mieści się solucja projektu, wykonaj poniższą komendę:
1
2
dotnet ef database update —project ApartmentRentral.Infrastructure —startup-project
ApartmentRental.API
W okienku konsoli/terminala możemy zobaczyć wykonane komendy na bazie danych z pliku migracyjnego:
Plik z bazą powinien się wygenerować w folderze AparmentRental.API.
Stworzoną bazę danych możemy obejrzeć chociażby za pomocą bezpłatnego narzędzia DbBrowser for Sqlite
lub w środowisku programistycznym JetBrains Rider
.
Zadania 3
- Usuń ze wszystkich projektów puste klasy
Class1.cs
- Usuń klasę WeatherForecast.cs z projektu ApartmentRental.API
- Wykonaną pracę zacommituj i pushnij na stworzone na poprzednich zajęciach repozytorium i wyślij informację o zakończeniu pracy do prowadzącego