Wprowadzenie

W artykule zostanie omówiony sposób modelowania tabel w relacji jeden do wielu oraz wiele do wielu. Postaram się pokazać co robi za nas Entity Framework oraz jak możemy wydajnie używać tej technologii w pracy z tabelami połączonymi relacjami.

Za każdym razem gdy staramy się modelować bazę danych zgodnie z wymaganiami aplikacji okazuje się, że tabele będą miały relacje pomiędzy sobą. Może wystąpić przypadek w którym dane z jednej tabeli mają związek z danymi w innej tabeli. W artykule omówię relacje pomiędzy tabelami oraz pokaże jak używać technologii Entity Framework w przypadku występienia takich relacji oraz współpracy z bazą danych.


Relacja jeden do wielu

Załóżmy, że mamy tablę A w której każdy wiersz będzie miał związek z wieloma wierszami w tabeli B (związek pomiędzy tabelami może być również przedstawiony odwrotnie: każdy wiersz w tabeli B będzie miał dokładnie jednego rodzica w tabeli A). Osiąga się to przez klucz obcy w tabeli B, który odnosi się do klucza głównego w tabeli A. Ten rodzaj relacji nazywa się relacją jeden do wielu.

Aby pokazać taką relację posłużymy się dwiema prostymi tabelami w bazie danych. Pierwsza tabela Rooms będzie zawierać informację o różnych pomieszczeniach w biurze. Druga tabela Assets będzie zawierała informację o rzeczach wartościowych w danym pokoju. Tabela Assets będzie przechowywała rzeczy wartościowe w danym pokoju, będzie więc musiała być kojarzona z tabelą Rooms. Jest to zrobione za pomocą relacji jeden do wielu pomiędzy tabelami w której tabela Assets zawiera klucz obcy odnoszą się do danego pokoju z tabeli Rooms.

Entity Framework - relacja jeden do wielu

Relacja wiele do wielu

Relacja wiele do wielu jest relacją w której wiersze z danej tabeli odnosza się do wielu wierszy w innej tabeli a jednocześnie wiersze z innych tabel odnoszą się do wielu wierszy z pierwszej tabeli. Taki scenariusz wymaga przygotowania relacji wielu do wielu. Jest ona zwykle modelowana przez przygotowanie dodatkowej tabeli która będzie zawierała w sobie klucz obcy z obu tabel a to pozwoli w prosty sposób śledzić relacje między oryginalnymi tabelami. Tabela ta zawiera jedynie kolumny niezdbędne do zamodelowania relacji a kolumny te będą stanowiły klucz złożony dla tej tabeli.

Postarajmy się zilustrować takie podejście. Załóżmy, że prowadzimy wiele projektów, które są składowane w tabeli Projects. Organizowane są codzienne spotkania dla wszystkich członków pracujących nad danym projektem – każdy projekt może wskazwać pokój do przeprowadzenia codziennego spotkania. W tym samym czasie może wystąpić prośba o rezerwację danego pokoju dla wielu projektów. Aby śledzić te rezerwacje musimy przygotować osobną tabelę, która będzie śledzić projekty i przynależność do pokoju. Zamodelowanie takiej relacji jest możliwe po dodaniu tabeli ProjectRooms:

Entity Framework - relacja wiele do wielu

Przygotowanie aplikacji

Nie będę zagłębiał się w szczególy tak jak w poprzednich artykułach związanych z Entity Framework. Skupimy się na rzeczach istotnych. Ponownie wrócimy do przygotowania aplikacji konsolowej (zdecydowanie łatwiej będzie pokazać przygotowany kod). W kolejnym kroku dodamy do naszego projektu ADO.NET Entity Data Model. Model zostanie wygenerowany z istniejącej bazy danych, której schemat został przedstawiony na powyższych diagramach.

Wygenerowany model bazy danych:

Entity Framework - model bazy danych

Relacja jeden do wielu – tworzenie encji

Na powyższym diagramie możecie zobaczyć, że Entity Framework był wystarczająco inteligenty, żeby zrozumieć, że pomiędzy tabelą Asstes a Rooms jest relacja jeden do wielu.

Tak wygenerowane encje pozwalają nam na wykonywanie wszystkich operacji CRUD na powiązanych encjach za pomocą właściwości związanych z oryginalną encją. W poniższej sekcji zostanie zaprezentowany sposób jak używać tych operacji.

Operacje CRUD na połączonych tabelach

W tej sekcji omówimy podstawowe opracje na encji Asset przy użyciu encji Room. Użyjemy encji Room a następnie właściowości związanej z encją Asset i spróbujemy wykonać wszystkie operacje.

SELECT

W pierwszej kolejności wyświetlimy w konsoli informację dotyczącą wszystkich pokoi a następnie użytkownik będzie mogł wskazać interesujący go pokój. Przykładowe działanie aplikacji:

using System;
using System.Linq;
namespace EF_RelationShips
{
    class Program
    {
        static void Main(string[] args)
        {
            DisplayAllData();
            SelectOperation();
        }
        static void SelectOperation()
        {
            Console.WriteLine();
            Console.Write("Wybierz pokój: ");
            int selectedRoom = Convert.ToInt32(Console.ReadLine());
            using (SampleDbEntities db = new SampleDbEntities())
            {
               // Pobieramy dane dla wskazanego przez użytkownika pokoju
                Rooms room = db.Rooms.SingleOrDefault<Rooms>(a => a.RoomID == selectedRoom);
                // wykorzystujemy relacje, aby pobrać wartościowe rzeczy z wskazanego pokoju
                var roomAssets = room.Assets;
                foreach (var item in roomAssets)
                {
                    Console.WriteLine($"Id: {item.AssetID}, Nazwa: {item.AssetName}, IdPokoju: {item.RoomID}");
                }
            }
            Console.ReadKey();
            // Wynik przykładowego działania
            // Id: 1, Nazwa: Room1
            // Id: 2, Nazwa: Room2
            // Id: 3, Nazwa: Room3
            // Wybierz pokój: 2
            // Id: 4, Nazwa: PC3, IdPokoju: 2
            // Id: 6, Nazwa: Projector 2, IdPokoju: 2
        }
        static void DisplayAllData()
        {
            using (SampleDbEntities db = new SampleDbEntities())
            {
                var data = db.Rooms;
                foreach (var item in data)
                {
                    Console.WriteLine($"Id: {item.RoomID}, Nazwa: {item.RoomName}");
                }
            }
        }
    }
}

INSERT

W tym przykładzie dokonamy dodania danych do tabeli Assets przy użyciu wskazanego przez użytkownika pokoju (aplikacja została nieznacznie zmieniona, aby obsługiwać kolejno dodawane operacje):
using System;
using System.Linq;
namespace EF_RelationShips
{
    class Program
    {
        static void Main(string[] args)
        {
            int selection = SelectYourOperation();
            switch (selection)
            {
                case 1:
                    Select();
                    break;
                case 2:
                    Insert();
                    break;
                default:
                    break;
            }
        }
        static void Insert()
        {
            DisplayAllData();
            Console.WriteLine();
            Console.Write("Wybierz pokój: ");
            int selectedRoom = Convert.ToInt32(Console.ReadLine());
            using (SampleDbEntities1 db = new SampleDbEntities1())
            {
                // Pobieramy dane dla wskazanego przez użytkownika pokoju
                Rooms room = db.Rooms.Single(a => a.RoomID == selectedRoom);
                // Tworzymy nowy obiekt Assets
                Assets asset = new Assets();
                asset.AssetName = "Dodane przez Insert";
                // wykorzystujemy relacje, aby dodać obiekt asset do odpowiedniego pokoju
                room.Assets.Add(asset);
                // zapsiujemy zmiany
                db.SaveChanges();
            }
        }
        static void Select()
        {
            DisplayAllData();
            Console.WriteLine();
            Console.Write("Wybierz pokój: ");
            int selectedRoom = Convert.ToInt32(Console.ReadLine());
            using (SampleDbEntities1 db = new SampleDbEntities1())
            {
               // Pobieramy dane dla wskazanego przez użytkownika pokoju
                Rooms room = db.Rooms.Single(a => a.RoomID == selectedRoom);
                // wykorzystujemy relacje, aby pobrać wartościowe rzeczy z wskazanego pokoju
                var roomAssets = room.Assets;
                foreach (var item in roomAssets)
                {
                    Console.WriteLine($"Id: {item.AssetID}, Nazwa: {item.AssetName}, IdPokoju: {item.RoomID}");
                }
            }
            Console.ReadKey();    
        }
        static void DisplayAllData()
        {
            using (SampleDbEntities1 db = new SampleDbEntities1())
            {
                var data = db.Rooms;
                foreach (var item in data)
                {
                    Console.WriteLine($"Id: {item.RoomID}, Nazwa: {item.RoomName}");
                }
            }
        }
        static int SelectYourOperation()
        {
            Console.WriteLine("1: Select");
            Console.WriteLine("2: Insert");
            Console.WriteLine();
            Console.Write("Wybierz operację: ");
            int selection = Convert.ToInt32(Console.ReadLine());
            return selection;
        }
    }
}

DELETE

Usuwanie danych z tabeli Assets może być również wykonane przez proste pobranie wybranego elementu dla wskazanego pokoju a następnie usunięcie go z tabeli Assets:
using System;
using System.Linq;
namespace EF_RelationShips
{
    class Program
    {
        static void Main(string[] args)
        {
            int selection = SelectYourOperation();
            switch (selection)
            {
                case 1:
                    Select();
                    break;
                case 2:
                    Insert();
                    break;
                case 3:
                    Delete();
                    break;
                default:
                    break;
            }
        }
        static void Delete()
        {
            DisplayAllData();
            Console.WriteLine();
            Console.Write("Wybierz pokój: ");
            int selectedRoom = Convert.ToInt32(Console.ReadLine());
            using (SampleDbEntities1 db = new SampleDbEntities1())
            {
                // Pobieramy dane dla wskazanego przez użytkownika pokoju
                Rooms room = db.Rooms.SingleOrDefault<Rooms>(a => a.RoomID == selectedRoom);
                // wykorzystujemy relacje, aby pobrać wartościowe rzeczy z wskazanego pokoju
                var roomAssets = room.Assets;
                foreach (var item in roomAssets)
                {
                    Console.WriteLine($"Id: {item.AssetID}, Nazwa: {item.AssetName}, IdPokoju: {item.RoomID}");
                }
                Console.WriteLine();
                Console.Write("Wybierz element do usunięcia: ");
                int selectedAsset = Convert.ToInt32(Console.ReadLine());
                // pobieramy wiersz do usunięcia z tabeli Assets
                Assets asset = db.Assets.SingleOrDefault<Assets>(a => a.AssetID == selectedAsset);
                // usuwamy wskazany element
                db.Assets.Remove(asset);
                // zapisujemy dane
                db.SaveChanges();
            }
        }
        static void Insert()
        {
            DisplayAllData();
            Console.WriteLine();
            Console.Write("Wybierz pokój: ");
            int selectedRoom = Convert.ToInt32(Console.ReadLine());
            using (SampleDbEntities1 db = new SampleDbEntities1())
            {
                // Pobieramy dane dla wskazanego przez użytkownika pokoju
                Rooms room = db.Rooms.Single(a => a.RoomID == selectedRoom);
                // Tworzymy nowy obiekt Assets
                Assets asset = new Assets();
                asset.AssetName = "Dodane przez Insert";
                // wykorzystujemy relacje, aby dodać obiekt asset do odpowiedniego pokoju
                room.Assets.Add(asset);
                // zapsiujemy zmiany
                db.SaveChanges();
            }
        }
        static void Select()
        {
            DisplayAllData();
            Console.WriteLine();
            Console.Write("Wybierz pokój: ");
            int selectedRoom = Convert.ToInt32(Console.ReadLine());
            using (SampleDbEntities1 db = new SampleDbEntities1())
            {
               // Pobieramy dane dla wskazanego przez użytkownika pokoju
                Rooms room = db.Rooms.Single(a => a.RoomID == selectedRoom);
                // wykorzystujemy relacje, aby pobrać wartościowe rzeczy z wskazanego pokoju
                var roomAssets = room.Assets;
                foreach (var item in roomAssets)
                {
                    Console.WriteLine($"Id: {item.AssetID}, Nazwa: {item.AssetName}, IdPokoju: {item.RoomID}");
                }
            }
            Console.ReadKey();
        }
        static void DisplayAllData()
        {
            using (SampleDbEntities1 db = new SampleDbEntities1())
            {
                var data = db.Rooms;
                foreach (var item in data)
                {
                    Console.WriteLine($"Id: {item.RoomID}, Nazwa: {item.RoomName}");
                }
            }
        }
        static int SelectYourOperation()
        {
            Console.WriteLine("1: Select");
            Console.WriteLine("2: Insert");
            Console.WriteLine("3: Delete");
            Console.WriteLine();
            Console.Write("Wybierz operację: ");
            int selection = Convert.ToInt32(Console.ReadLine());
            return selection;
        }
    }
}

UPDATE

Podobnie jak w przypadku poprzednich operacji, Update może być również przeprowadzony za pomocą tabel połączonych:
using System;
using System.Linq;
namespace EF_RelationShips
{
    class Program
    {
        static void Main(string[] args)
        {
            int selection = SelectYourOperation();
            switch (selection)
            {
                case 1:
                    Select();
                    break;
                case 2:
                    Insert();
                    break;
                case 3:
                    Delete();
                    break;
                case 4:
                    Update();
                    break;
                default:
                    break;
            }
        }
        static void Update()
        {
            DisplayAllData();
            Console.WriteLine();
            Console.Write("Wybierz pokój: ");
            int selectedRoom = Convert.ToInt32(Console.ReadLine());
            using (SampleDbEntities1 db = new SampleDbEntities1())
            {
                // Pobieramy dane dla wskazanego przez użytkownika pokoju
                Rooms room = db.Rooms.SingleOrDefault<Rooms>(a => a.RoomID == selectedRoom);
                // wykorzystujemy relacje, aby pobrać wartościowe rzeczy z wskazanego pokoju
                var roomAssets = room.Assets;
                foreach (var item in roomAssets)
                {
                    Console.WriteLine($"Id: {item.AssetID}, Nazwa: {item.AssetName}, IdPokoju: {item.RoomID}");
                }
                Console.WriteLine();
                Console.Write("Wybierz element do aktualizacji: ");
                int selectedAsset = Convert.ToInt32(Console.ReadLine());
                // pobieramy wiersz do aktualizacji z tabeli Assets
                Assets asset = db.Assets.SingleOrDefault<Assets>(a => a.AssetID == selectedAsset);
                asset.AssetName = "Po aktualizacji";
                // zapisujemy dane
                db.SaveChanges();
            }
        }
        static void Delete()
        {
            DisplayAllData();
            Console.WriteLine();
            Console.Write("Wybierz pokój: ");
            int selectedRoom = Convert.ToInt32(Console.ReadLine());
            using (SampleDbEntities1 db = new SampleDbEntities1())
            {
                // Pobieramy dane dla wskazanego przez użytkownika pokoju
                Rooms room = db.Rooms.SingleOrDefault<Rooms>(a => a.RoomID == selectedRoom);
                // wykorzystujemy relacje, aby pobrać wartościowe rzeczy z wskazanego pokoju
                var roomAssets = room.Assets;
                foreach (var item in roomAssets)
                {
                    Console.WriteLine($"Id: {item.AssetID}, Nazwa: {item.AssetName}, IdPokoju: {item.RoomID}");
                }
                Console.WriteLine();
                Console.Write("Wybierz element do usunięcia: ");
                int selectedAsset = Convert.ToInt32(Console.ReadLine());
                // pobieramy wiersz do usunięcia z tabeli Assets
                Assets asset = db.Assets.SingleOrDefault<Assets>(a => a.AssetID == selectedAsset);
                // usuwamy wskazany element
                db.Assets.Remove(asset);
                // zapisujemy dane
                db.SaveChanges();
            }
        }
        static void Insert()
        {
            DisplayAllData();
            Console.WriteLine();
            Console.Write("Wybierz pokój: ");
            int selectedRoom = Convert.ToInt32(Console.ReadLine());
            using (SampleDbEntities1 db = new SampleDbEntities1())
            {
                // Pobieramy dane dla wskazanego przez użytkownika pokoju
                Rooms room = db.Rooms.Single(a => a.RoomID == selectedRoom);
                // Tworzymy nowy obiekt Assets
                Assets asset = new Assets();
                asset.AssetName = "Dodane przez Insert";
                // wykorzystujemy relacje, aby dodać obiekt asset do odpowiedniego pokoju
                room.Assets.Add(asset);
                // zapsiujemy zmiany
                db.SaveChanges();
            }
        }
        static void Select()
        {
            DisplayAllData();
            Console.WriteLine();
            Console.Write("Wybierz pokój: ");
            int selectedRoom = Convert.ToInt32(Console.ReadLine());
            using (SampleDbEntities1 db = new SampleDbEntities1())
            {
               // Pobieramy dane dla wskazanego przez użytkownika pokoju
                Rooms room = db.Rooms.Single(a => a.RoomID == selectedRoom);
                // wykorzystujemy relacje, aby pobrać wartościowe rzeczy z wskazanego pokoju
                var roomAssets = room.Assets;
                foreach (var item in roomAssets)
                {
                    Console.WriteLine($"Id: {item.AssetID}, Nazwa: {item.AssetName}, IdPokoju: {item.RoomID}");
                }
            }
            Console.ReadKey();
        }
        static void DisplayAllData()
        {
            using (SampleDbEntities1 db = new SampleDbEntities1())
            {
                var data = db.Rooms;
                foreach (var item in data)
                {
                    Console.WriteLine($"Id: {item.RoomID}, Nazwa: {item.RoomName}");
                }
            }
        }
        static int SelectYourOperation()
        {
            Console.WriteLine("1: Select");
            Console.WriteLine("2: Insert");
            Console.WriteLine("3: Delete");
            Console.WriteLine("4: Update");
            Console.WriteLine();
            Console.Write("Wybierz operację: ");
            int selection = Convert.ToInt32(Console.ReadLine());
            return selection;
        }
    }
}


Relacja wiele do wielu – tworzenie encji

Podczas genererowania encji z bazy danych czegoś nam zabrakło…

Dla tabeli ProjectRooms nie została wygenerowana odpowiednia encja. Dlaczego? Jeżeli spojrzymy na encje zauważymy, że Entity Framework zrobił bardzo madrą rzecz. Technologia ta rozumie, że tabela została wygenerowana, aby zamodelować relację wiele do wielu. Relacja ta została odzwierciedlona poprzez wygenerowanie właściwości nawigacyjnych dla poszczególnych encji pozwalając przy tym aby kod aplikacji był zdolny poradzić sobie z taką relacją.

W prostych słowach, Entity Framework odczytuje tabelę ProjectRooms a następnie tworzy właściwość Project w Rooms oraz właściwość Rooms w encji Projects.

Spróbujmy teraz wykonać niektóre z powyższych operacji wykorzystując do tego właściwości nawigacyjne stworzone przez Entity Framework.

using System;
using System.Linq;
namespace EF_Relationships_ManyToMany
{
    class Program
    {
        static void Main(string[] args)
        {
            DisplayAllData();
            // W celu ułatwienia interpretacji kodu zdefiniujemy na podstawie powyższego rezultatu
            // idProjektu oraz pobierzemy pokoje do niego przypisane
            using (SampleDbEntities db = new SampleDbEntities())
            {
                // pobieramy dane dotyczące wskazanego projektu
                Projects sampleProject = db.Projects.Single(a => a.ProjectID == 1);
                // pobieramy pokoje przynależne do danego projektu
                var data = sampleProject.Rooms;
                // do naszego projektu dodajemy kolejny pokój
                sampleProject.Rooms.Add(db.Rooms.Single(a => a.RoomID == 2));
                // zapisujemy zmiany
                db.SaveChanges();
            }
            Console.WriteLine();
            // Wyświetlimy raz jeszcze wszystkie dane w celu sprawdzenia poprawności działania
            DisplayAllData();
            // Wynik wywołania kodu po zmianach
            // Id: 1, Nazwa: Project 1, IdPokoju: 1
            // Id: 1, Nazwa: Project 1, IdPokoju: 2
            // Id: 1, Nazwa: Project 1, IdPokoju: 3
            // Id: 2, Nazwa: Project 2, IdPokoju: 1
            // Id: 2, Nazwa: Project 2, IdPokoju: 2
            // Id: 2, Nazwa: Project 2, IdPokoju: 3
        }
        static void DisplayAllData()
        {
            using (SampleDbEntities db = new SampleDbEntities())
            {
                // w pierwszej kolejności zwracamy wszystkie projekty
                var projects = db.Projects;
                foreach (var item in projects)
                {
                    // wykorzystując relacje sprawdzamy IdPokoju do którego przypisany jest dany projekt
                    foreach (var item2 in item.Rooms)
                    {
                        Console.WriteLine($"Id: {item.ProjectID}, Nazwa: {item.ProjectName}, IdPokoju: {item2.RoomID}");
                    }
                }
            }
            // Wynik wywołania kodu przed zmianami
            // Id: 1, Nazwa: Project 1, IdPokoju: 1
            // Id: 1, Nazwa: Project 1, IdPokoju: 3
            // Id: 2, Nazwa: Project 2, IdPokoju: 1
            // Id: 2, Nazwa: Project 2, IdPokoju: 2
            // Id: 2, Nazwa: Project 2, IdPokoju: 3
            Console.ReadKey();
        }
    }
}
Gdybyśmy chcieli teraz usunąć dodany przez nasz pokój do projektu wystarczy wywołać poniższy kod:
sampleProject.Rooms.Remove(db.Rooms.Single(a => a.RoomID == 2));
W celu łatwiejszej interpretacji kodu dane te zotały zapisane na sztywno – nigdy nie jest to dobre rozwiązanie problemu – tutaj jednak został przygotowany najprostszy możliwy przykład.

Z powyższego artykułu mogliście się dowiedzieć jak używać Entity Framework do modelowania oraz używania relacji jeden do wielu oraz wiele do wielu. Kod tego przykładu został napisany jako aplikacja konsolowa co jest zdecydowanie łatwiejsze do przedstawienia w postaci kompletnego kodu. W ramach praktyki polecam samemu napisać podobną aplikację przy użyciu technologii ASP.NET MVC - będzie trzeba przygotować widoki, obsługę wyboru danego projektu czy pokoju oraz operację usuwania i dodawania danych do bazy danych.