EF: Relacje między tabelami – Code-First

Posted by Przemysław Owsianik on 2021-10-03
<p><a href="https://en.wikipedia.org/wiki/Object-relational_mapping">ORM</a>&nbsp;taki jak&nbsp;Entity Framework, mocno ułatwia nam pracę w kodzie z bazą danych (goodbye ADO.NET 🙂 ), ale nie rozwiązuje za nas problemu zaprojektowania takiej bazy. Jedną z podstawowych kwestii przed jakimi stajemy jest odpowiedź na pytanie o sposób łączenia tabel. W tym poście nie będę pisał jednak o projektowaniu baz danych, (przy założeniu, że masz pomysł na swoją bazę ;)) a o sposobie implementacji znanych wszystkim relacji w podejściu&nbsp;Code-First.</p> <p>Na początek przypomnijmy sobie jakimi, w relacyjnych bazach danych, relacjami (a jakże :)) mogą być związane tabele.</p> <p><b>Jeden do jednego</b></p> <p>Jednemu wierszowi w pierwszej tabeli, odpowiada jeden i tylko jeden wiersz z drugiej tabeli.</p> <p><b>Jeden do wielu</b></p> <p>Jednemu wierszowi w pierwszej tabeli, może odpowiadać wiele wierszy z drugiej tabeli.</p> <p><b>Wiele do wielu</b></p> <p>Jak sama nazwa wskazuje :). Taką tabelę zwykle najkorzystniej jest podzielić, przy użyciu tzw. Tabeli łączącej na relację jeden do wielu.</p> --- <p>Teraz zobaczmy jak zaimplementować te relacje w EF w podejściu Code-First:</p> <h2>Relacja jeden do jednego:</h2> <p>Załóżmy, że chcemy użyć tej relacji do połączenia podstawowych danych ucznia i jego średniej. Tabele nich nazywają się&nbsp;Student&nbsp;i&nbsp;AverageGrade. Jeden uczeń może mieć tylko jedną średnią ocen :).</p> <p>W EF kolumny tabel są reprezentowane przez właściwości. Nasze klasy reprezentujące tabele i mające być połączone relacją jeden do jeden, powinny oprócz właściwych sobie kolumn zawierać wirtualną składową właściwość, reprezentującą powiązaną tabelę. Czyli w klasie Student powinna być:</p> <pre class="language-" tabindex="0"><code class="language-">public virtual AverageGrade AverageGrade {get; set;} </code></pre> <p>a w klasie AverageGrade:</p> <pre class="language-" tabindex="0"><code class="language-">public virtual Student Student {get; set;} </code></pre> <p>W uproszczeniu i pomijając ficzery takie jak DataAnnotations,&nbsp;klasy mogłyby wyglądać w ten sposób:</p> <pre class="language-" tabindex="0"><code class="language-">public class Student { public int Id {get; set;} public string Name {get; set;} public virtual AverageGrade AverageGrade {get; set;} } public class AverageGrade { public int Id {get; set;} public float value {get; set;} public virtual Student Student {get; set;} } </code></pre> <h2>Relacja jeden do wielu:</h2> <p>Teraz zmieńmy sobie kontekst :). Naszymi tabelami niech będą&nbsp;Employer&nbsp;i&nbsp;Worker.&nbsp;Jeden pracodawca może zatrudniać wielu pracowników, ale jeden pracownik ma tylko jednego pracodawcę.</p> <p>W takim wypadku klasa reprezentująca tabelę z pracownikami powinna zawierać, oprócz swoich zwykłych właściwości, publiczną wirtualną właściwość pozwalającą na przypisanie do niej implementacji interfejsu generycznego&nbsp;ICollection&lt;T&gt;&nbsp;&nbsp;gdzie T to typ przechowywanych elementów, w tym wypadku&nbsp;Worker:</p> <pre class="language-" tabindex="0"><code class="language-">public virtual ICollection<Worker> Workers{get; private set;} </code></pre> <p>Tą kolekcję powinno się zainicjalizować w konstruktorze naszej klasy. Worker natomiast powinien, tak jak w przypadku relacji jeden do jeden, posiadać odwołanie do klasy Employer:</p> <pre class="language-" tabindex="0"><code class="language-">public virtual Employer Employer {get; private set;} </code></pre> <p>Obie klasy w uproszczeniu mogłyby wyglądać tak:</p> <pre class="language-" tabindex="0"><code class="language-">public class Employer { public Employer() { this.Workers = new HashSet<Worker>(); } public int Id {get; set;} public decimal NominalCapital {get; set;} public string CompanyName {get; set;} public virtual ICollection<Worker> Workers {get; set;} } public class Woker { public int Id {get; set;} public string Name {get; set;} public virtual Employer Employer {get; set;} } </code></pre> <h2>Relacja wiele&nbsp;do wielu:</h2> <p>Na koniec została nam powszechna, choć niewdzięczna, relacja do właściwego zastosowania której, zwykle używamy trzech tabel. Dwóch głównych i jednej tzw.&nbsp;tabeli łączącej.&nbsp;Tabela łącząca służy do rozbicia relacji, na dwie jeden do wielu. W Entity Framework tabela łącząca jest tworzona automatycznie, o ile poprawnie zaimplementujemy klasy mające odzwierciedlać relację wiele do wielu. Przyjrzyjmy się tabelom:&nbsp;TrainginDay&nbsp;i&nbsp;Exercise. Jeden dzień treningowy, może mieć wiele ćwiczeń, tak samo jak jedno ćwiczenie może być przypisane do wielu dni treningowych. Obie klasy reprezentujące te tabele, powinny zawierać referencje do ICollection&lt;T&gt; (jak w jeden do wielu). Co do utworzenia&nbsp;tych kolekcji, to wystarczy, że zainicjujemy tylko jedną z nich.</p> <p>Prosta implementacja tych klas mogłaby wyglądać tak:</p> <pre class="language-" tabindex="0"><code class="language-">public class Exercise { public Exercise() { this.TrainingDays = new HashSet<TrainingDay>(); } public int Id {get; set;} public string Name {get; set;} public string Description {get; set;} public virtual ICollection<TrainingDay> TrainingDays{get; set;} } public class TrainingDay { public int Id {get; set;} public int IdOfPlan {get; set;} public int IdOfDayOfWeek {get; set;} public virtual ICollection<Exercise> Exercises{get; set;} } </code></pre> <p>EF utworzy za nas tabelę łączącą&nbsp;TrainingDayExcersise&nbsp;zawierającą tylko klucze główne do obu tych tabel.</p> <p>Jeżeli chodzi o relacje między tabelami w Entity Framework, to chyba udało mi się zawrzeć tu to co jest najważniejsze. W bliskiej przyszłości napiszę co nieco o czymś co nazywa się DataAnnotations. W praktyce są to atrybuty, pozwalające nakładać na nasze kolumny min. ograniczenia, czy określać klucze.</p>