Zrozumienie relacji modelowych w Laravel Eloquent

Modele i ich relacje są sercem Laravel Eloquent. Jeśli sprawiają ci trudność lub nie możesz znaleźć prostego, przyjaznego i kompletnego przewodnika, zacznij tutaj!

Siedząc po drugiej stronie swojego artykułu programistycznego, pisarzowi łatwo jest udawać lub wysadzać w powietrze aurę ekspertyzy/prestiżu, jaką zapewnia platforma. Ale będę szczery – było mi bardzo ciężko nauka Laravela, choćby dlatego, że był to mój pierwszy pełny framework. Jednym z powodów było to, że nie używałem go w pracy i badałem go z ciekawości; więc podjąłem próbę, doszedłem do sedna, pogubiłem się, poddałem się iw końcu zapomniałem o wszystkim. Musiałem to zrobić 5-6 razy, zanim zaczęło to mieć dla mnie sens (oczywiście dokumentacja nie pomaga).

Ale to, co nadal nie miało sensu, to Eloquent. A przynajmniej relacje między modelami (ponieważ Eloquent jest zbyt duży, aby się go w całości nauczyć). Przykłady autorów modeli i postów na blogach to żart, ponieważ prawdziwe projekty są znacznie bardziej złożone; niestety oficjalne dokumenty używają tych samych (lub podobnych) przykładów. Lub nawet jeśli natknąłem się na jakiś przydatny artykuł/zasób, wyjaśnienie było tak złe lub tak bardzo brakowało, że było po prostu bezużyteczne.

(Nawiasem mówiąc, byłem już wcześniej atakowany za atakowanie oficjalnej dokumentacji, więc jeśli masz podobne pomysły, oto moja standardowa odpowiedź: sprawdź dokumentację Django, a następnie porozmawiaj ze mną.)

W końcu, kawałek po kawałku, wszystko się połączyło i nabrało sensu. W końcu mogłem poprawnie modelować projekty i wygodnie korzystać z modeli. Pewnego dnia natknąłem się na kilka zgrabnych sztuczek Collections, które uprzyjemniają tę pracę. W tym artykule zamierzam omówić to wszystko, zaczynając od podstaw, a następnie omawiając wszystkie możliwe przypadki użycia, które napotkasz w prawdziwych projektach.

Dlaczego relacje z modelami Eloquent są trudne?

Niestety, spotykam zbyt wielu programistów Laravel, którzy nie rozumieją właściwie modeli.

Ale dlaczego?

Nawet dzisiaj, kiedy następuje eksplozja kursów, artykułów i filmów na temat Laravela, ogólne zrozumienie jest słabe. Myślę, że to ważna kwestia i warta zastanowienia.

Jeśli mnie zapytasz, powiem, że Elokwentne relacje modelowe wcale nie są trudne. Przynajmniej patrząc z perspektywy definicji „twardego”. Migracje schematów na żywo są trudne; pisanie nowego silnika szablonów jest trudne; wniesienie kodu do rdzenia Laravel jest trudne. W porównaniu z nimi nauka i używanie ORM . . . cóż, to nie może być trudne! 🤭🤭

W rzeczywistości programiści PHP uczący się Laravela mają trudności z Eloquent. To jest prawdziwy podstawowy problem i moim zdaniem składa się na to kilka czynników (ostra, niepopularna opinia!):

  • Przed Laravelem większość programistów PHP miała do czynienia ze środowiskiem CodeIgniter (nadal żywy, nawiasem mówiąc, nawet jeśli stał się bardziej podobny do Laravel/CakePHP). W starszej społeczności CodeIgniter (jeśli taka istniała) „najlepszą praktyką” było bezpośrednie umieszczanie zapytań SQL tam, gdzie było to potrzebne. I chociaż dzisiaj mamy nowego CodeIgnitera, nawyki zostały przeniesione. W rezultacie podczas nauki Laravela idea ORM jest w 100% nowa dla programistów PHP.
  • Pomijając bardzo mały procent PHP wystawiony na działanie frameworków, takich jak Yii, CakePHP itp., pozostałe są przyzwyczajone do pracy w rdzeniu PHP lub w środowisku takim jak WordPress. I tutaj znowu nie istnieje sposób myślenia oparty na OOP, więc framework, kontener usług, wzorzec projektowy, ORM. . . to są obce koncepcje.
  • W świecie PHP istnieje niewiele koncepcji ciągłego uczenia się. Przeciętny programista jest zadowolony z pracy z konfiguracjami z jednym serwerem przy użyciu relacyjnych baz danych i wydawania zapytań zapisanych jako ciągi znaków. Programowanie asynchroniczne, gniazda sieciowe, HTTP 2/3, Linux (zapomnij o Dockerze), testy jednostkowe, projektowanie oparte na domenach — to wszystko są obce pomysły przeważającej części programistów PHP. W rezultacie czytanie czegoś nowego i wymagającego do tego stopnia, że ​​jest to wygodne, nie zdarza się, gdy napotyka się Eloquent.
  • Ogólne zrozumienie baz danych i modelowania jest również słabe. Ponieważ projektowanie baz danych jest bezpośrednio i nierozerwalnie związane z modelami Eloquent, podnosi poprzeczkę trudności wyżej.

Nie chcę być surowy i uogólniać globalnie — są też znakomici programiści PHP i jest ich wielu, ale ich ogólny odsetek jest bardzo niski.

Jeśli to czytasz, oznacza to, że przekroczyłeś wszystkie te bariery, natknąłeś się na Laravela i zadarłeś z Eloquent.

Gratulacje! 👏

Jesteś prawie na miejscu. Wszystkie elementy składowe są na swoim miejscu i musimy tylko przejść przez nie we właściwej kolejności i szczegółowo. Innymi słowy, zacznijmy od poziomu bazy danych.

Modele baz danych: relacje i liczność

Aby uprościć sprawę, załóżmy, że w tym artykule pracujemy tylko z relacyjnymi bazami danych. Jednym z powodów jest to, że ORM zostały pierwotnie opracowane dla relacyjnych baz danych; innym powodem jest to, że RDBMS są nadal przytłaczająco popularne.

Model danych

Najpierw lepiej zrozummy modele danych. Idea modelu (a dokładniej modelu danych) pochodzi z bazy danych. Brak bazy danych, brak danych, a więc brak modelu danych. A co to jest model danych? Po prostu jest to sposób, w jaki decydujesz się przechowywać/strukturyzować swoje dane. Na przykład w sklepie e-commerce możesz przechowywać wszystko w jednej gigantycznej tabeli (STRASZNA praktyka, ale niestety nierzadka w świecie PHP); to byłby twój model danych. Możesz także podzielić dane na 20 głównych i 16 tabel łączących; to także model danych.

Należy również zauważyć, że struktura danych w bazie danych nie musi odpowiadać w 100% ich rozmieszczeniu w ORM frameworka. Jednak zawsze staramy się trzymać rzeczy tak blisko, jak to możliwe, abyśmy nie musieli pamiętać o jeszcze jednej rzeczy podczas opracowywania.

Kardynalność

Usuńmy też szybko ten termin: kardynalność. Odnosi się to po prostu do „liczenia”, luźno mówiąc. A więc 1, 2, 3. . . wszystko może być kardynalnością czegoś. Koniec opowieści. Ruszajmy się!

Relacje

Teraz, gdy przechowujemy dane w dowolnym typie systemu, istnieją sposoby, w jakie punkty danych mogą być ze sobą powiązane. Wiem, że brzmi to abstrakcyjnie i nudno, ale poczekaj trochę. Sposoby łączenia różnych elementów danych nazywane są relacjami. Przyjrzyjmy się najpierw przykładom niezwiązanym z bazami danych, abyśmy byli przekonani, że w pełni rozumiemy ten pomysł.

  • Jeśli przechowujemy wszystko w tablicy, jedną z możliwych zależności jest: następny element danych ma indeks większy niż poprzedni indeks o 1.
  • Jeśli przechowujemy dane w drzewie binarnym, jedną z możliwych zależności jest to, że drzewo potomne po lewej stronie zawsze ma mniejsze wartości niż węzeł nadrzędny (jeśli zdecydujemy się zachować drzewo w ten sposób).
  • Jeśli przechowujemy dane jako tablicę tablic o równej długości, możemy naśladować macierz, a wtedy jej właściwości stają się relacjami dla naszych danych.

Widzimy więc, że słowo „relacja” w kontekście danych nie ma ustalonego znaczenia. W rzeczywistości, gdyby dwie osoby patrzyły na te same dane, mogłyby zidentyfikować dwie bardzo różne relacje między danymi (cześć, statystyki!) i obie z nich mogłyby być prawidłowe.

Relacyjne bazy danych

Bazując na wszystkich pojęciach, które omawialiśmy do tej pory, możemy w końcu mówić o czymś, co ma bezpośrednie powiązanie z modelami we frameworku internetowym (Laravel) — relacyjnych bazach danych. Dla większości z nas podstawową używaną bazą danych jest MySQL, MariaDB, PostgreSQL, MSSQL, SQL Server, SQLite lub coś w tym stylu. Moglibyśmy również niejasno wiedzieć, że nazywają się one RDBMS, ale większość z nas zapomniała, co to właściwie oznacza i dlaczego ma to znaczenie.

„R” w RDBMS oznacza oczywiście relacyjny. To nie jest arbitralnie wybrany termin; w ten sposób podkreślamy fakt, że te systemy baz danych są zaprojektowane do wydajnej pracy z relacjami między przechowywanymi danymi. W rzeczywistości „relacja” ma tutaj ściśle matematyczne znaczenie i chociaż żaden programista nie musi się tym przejmować, warto wiedzieć, że istnieje ścisły matematyczny Fundacja pod tego typu bazami danych.

Zapoznaj się z tymi zasobami, aby poznać SQL i NoSQL.

Ok, więc wiemy z doświadczenia, że ​​dane w RDBMS są przechowywane jako tabele. Gdzie w takim razie są relacje?

Typy relacji w RDBMS

To chyba najważniejsza część całego tematu Laravela i relacji modelowych. Jeśli tego nie rozumiesz, Eloquent nigdy nie będzie miało sensu, więc proszę, uważaj przez następne kilka minut (to nawet nie jest takie trudne).

RDBMS pozwala nam na relacje między danymi — na poziomie bazy danych. Oznacza to, że te relacje nie są niepraktyczne/wyimaginowane/subiektywne i mogą być tworzone lub wywnioskowane przez różne osoby z tym samym skutkiem.

Jednocześnie istnieją pewne możliwości/narzędzia w ramach RDBMS, które pozwalają nam tworzyć i egzekwować te relacje, takie jak:

  • Główny klucz
  • Klucz obcy
  • Ograniczenia

Nie chcę, aby ten artykuł stał się kursem baz danych, więc zakładam, że wiesz, czym są te pojęcia. Jeśli nie, lub jeśli czujesz się chwiejny w swojej pewności siebie, polecam ten przyjazny film (zapraszam do zapoznania się z całą serią):

Tak się składa, że ​​te relacje w stylu RDBMS są również najczęstszymi relacjami występującymi w rzeczywistych aplikacjach (nie zawsze, ponieważ sieć społecznościową najlepiej modelować jako wykres, a nie jako zbiór tabel). Przyjrzyjmy się im więc po kolei, a także spróbujmy zrozumieć, gdzie mogą być przydatne.

Relacja jeden do jednego

W prawie każdej aplikacji internetowej istnieją konta użytkowników. Ponadto prawdziwe są (ogólnie mówiąc) stwierdzenia dotyczące użytkowników i kont:

  • Użytkownik może posiadać tylko jedno konto.
  • Konto może należeć tylko do jednego użytkownika.

Tak, możemy argumentować, że osoba może zarejestrować się za pomocą innego adresu e-mail i w ten sposób utworzyć dwa konta, ale z perspektywy aplikacji internetowej są to dwie różne osoby z dwoma różnymi kontami. Aplikacja nie pokaże np. danych jednego konta w innym.

Co to wszystko oznacza — jeśli masz taką sytuację w swojej aplikacji i używasz relacyjnej bazy danych, musisz zaprojektować ją jako relację jeden do jednego. Zauważ, że nikt cię sztucznie nie zmusza — w domenie biznesowej jest jasna sytuacja i tak się składa, że ​​korzystasz z relacyjnej bazy danych. . . tylko wtedy, gdy oba te warunki są spełnione, sięgasz po relację jeden do jednego.

W tym przykładzie (użytkownicy i konta) w ten sposób możemy zaimplementować tę relację podczas tworzenia schematu:

CREATE TABLE users(
    id INT NOT NULL AUTO_INCREMENT,
    email VARCHAR(100) NOT NULL,
    password VARCHAR(100) NOT NULL,
    PRIMARY KEY(id)
);

CREATE TABLE accounts(
    id INT NOT NULL AUTO_INCREMENT,
    role VARCHAR(50) NOT NULL,
    PRIMARY KEY(id),
    FOREIGN KEY(id) REFERENCES users(id)
);

Zauważ sztuczkę tutaj? Jest to dość rzadkie podczas tworzenia aplikacji, ale w tabeli kont mamy ustawiony identyfikator pola zarówno jako klucz podstawowy, jak i klucz obcy! Właściwość klucza obcego łączy ją z tabelą użytkowników (oczywiście 🙄), podczas gdy właściwość klucza podstawowego sprawia, że ​​kolumna id jest unikalna — prawdziwa relacja jeden do jednego!

To prawda, że ​​wierność tego związku nie jest gwarantowana. Na przykład nic nie stoi na przeszkodzie, aby dodać 200 nowych użytkowników bez dodania ani jednego wpisu do tabeli kont. Jeśli to zrobię, skończę z relacją jeden do zera! 🤭🤭 Ale w granicach czystej struktury to najlepsze, co możemy zrobić. Jeśli chcemy zapobiec dodawaniu użytkowników bez kont, musimy skorzystać z jakiejś logiki programistycznej, albo w postaci wyzwalaczy bazy danych, albo walidacji wymuszonych przez Laravel.

Jeśli zaczynasz się stresować, mam kilka bardzo dobrych rad:

  • Zrób to wolno. Tak wolno, jak trzeba. Zamiast próbować dokończyć ten artykuł i 15 innych, które dodałeś do zakładek na dzisiaj, trzymaj się tego. Niech zajmie to 3, 4, 5 dni, jeśli tego potrzebujesz — Twoim celem powinno być wyeliminowanie relacji Eloquent z Twojej listy na zawsze. Przeskakiwałeś już od artykułu do artykułu, marnując kilkaset godzin, a to nie pomogło. Więc tym razem zrób coś innego. 😇
  • Chociaż ten artykuł dotyczy Laravel Eloquent, wszystko to dzieje się znacznie później. Podstawą tego wszystkiego jest schemat bazy danych, więc powinniśmy skupić się na tym, aby najpierw zrobić to dobrze. Jeśli nie możesz pracować wyłącznie na poziomie bazy danych (zakładając, że na świecie nie ma frameworków), modele i relacje nigdy nie będą miały pełnego sensu. Więc zapomnij na razie o Laravelu. Całkowicie. Na razie tylko mówimy i zajmujemy się projektowaniem baz danych. Tak, od czasu do czasu będę robił odniesienia do Laravela, ale twoim zadaniem jest całkowite ich zignorowanie, jeśli komplikują ci obraz.
  • Później przeczytaj trochę więcej o bazach danych i ich ofercie. Indeksy, wydajność, wyzwalacze, podstawowe struktury danych i ich zachowanie, buforowanie, relacje w MongoDB. . . wszelkie styczne tematy, które możesz omówić, pomogą ci jako inżynierowi. Pamiętaj, że modele szkieletowe to tylko powłoki duchów; prawdziwa funkcjonalność platformy pochodzi z jej podstawowych baz danych.

Relacja jeden do wielu

Nie jestem pewien, czy zdajesz sobie z tego sprawę, ale jest to rodzaj relacji, którą wszyscy intuicyjnie tworzymy w naszej codziennej pracy. Kiedy tworzymy tabelę zamówień (hipotetyczny przykład), na przykład w celu przechowywania klucza obcego do tabeli użytkowników, tworzymy relację jeden-do-wielu między użytkownikami a zamówieniami. Dlaczego? Cóż, spójrz na to jeszcze raz z perspektywy tego, kto i ile może mieć: jeden użytkownik może mieć więcej niż jedno zamówienie, czyli mniej więcej tak działa cały e-commerce. A patrząc z przeciwnej strony, relacja mówi, że zamówienie może należeć tylko do jednego użytkownika, co też ma sens.

W modelowaniu danych, książkach RDBMS i dokumentacji systemowej sytuacja ta jest przedstawiona schematycznie w następujący sposób:

Zwróć uwagę na trzy linie tworzące coś w rodzaju trójzębu? To jest symbol „wielu”, więc ten diagram mówi, że jeden użytkownik może mieć wiele zamówień.

Nawiasem mówiąc, te liczby „wiele” i „jeden”, z którymi spotykamy się wielokrotnie, to tak zwana liczność relacji (pamiętasz to słowo z poprzedniej sekcji?). Ponownie, w tym artykule termin ten nie ma zastosowania, ale pomaga poznać tę koncepcję na wypadek, gdyby pojawiła się podczas wywiadów lub dalszej lektury.

Proste, prawda? A jeśli chodzi o rzeczywisty SQL, tworzenie tej relacji jest również proste. W rzeczywistości jest to o wiele prostsze niż w przypadku relacji jeden do jednego!

CREATE TABLE users( 
    id INT NOT NULL AUTO_INCREMENT, 
    email VARCHAR(100) NOT NULL, 
    password VARCHAR(100) NOT NULL, 
    PRIMARY KEY(id) 
);

CREATE TABLE orders( 
    id INT NOT NULL AUTO_INCREMENT, 
    user_id INT NOT NULL, 
    description VARCHAR(50) NOT NULL, 
    PRIMARY KEY(id), 
    FOREIGN KEY(user_id) REFERENCES users(id) 
);

Tabela zamówień przechowuje identyfikatory użytkowników dla każdego zamówienia. Ponieważ nie ma ograniczenia (ograniczenia), że identyfikatory użytkowników w tabeli zamówień muszą być unikalne, oznacza to, że możemy powtarzać jeden identyfikator wiele razy. To właśnie tworzy relację jeden-do-wielu, a nie jakaś tajemna magia, która jest ukryta pod spodem. Identyfikatory użytkowników są przechowywane w jakiś głupi sposób w tabeli zamówień, a SQL nie ma żadnej koncepcji jeden do wielu, jeden do jednego itd. Ale kiedy przechowujemy dane w ten sposób, można pomyśleć o relacji jeden-do-wielu.

Mam nadzieję, że teraz ma to sens. A przynajmniej więcej sensu niż wcześniej. 😅 Pamiętaj, że tak jak wszystko inne, jest to zwykła kwestia praktyki, a gdy zrobisz to 4-5 razy w rzeczywistych sytuacjach, nawet o tym nie pomyślisz.

Relacje wiele do wielu

Kolejnym rodzajem relacji, który pojawia się w praktyce, jest tak zwana relacja wiele-do-wielu. Jeszcze raz, zanim zaczniemy martwić się o frameworki lub nawet zagłębić się w bazy danych, pomyślmy o analogu w świecie rzeczywistym: książkach i autorach. Pomyśl o swoim ulubionym autorze; napisali więcej niż jedną książkę, prawda? Jednocześnie dość często zdarza się, że kilku autorów współpracuje nad książką (przynajmniej w gatunku non-fiction). Tak więc jeden autor może napisać wiele książek, a wielu autorów może napisać jedną książkę. Pomiędzy dwiema jednostkami (książką i autorem) tworzy się relacja wiele-do-wielu.

Teraz, biorąc pod uwagę, że jest mało prawdopodobne, aby stworzyć rzeczywistą aplikację obejmującą biblioteki, książki i autorów, pomyślmy o kilku innych przykładach. W ustawieniu B2B producent zamawia towary od dostawcy i w zamian otrzymuje fakturę. Faktura będzie zawierać kilka pozycji, z których każda będzie zawierała ilość i dostarczoną pozycję; na przykład 5-calowe kawałki rury x 200 itp. W tej sytuacji pozycje i faktury mają relację wiele do wielu (przemyśl to i przekonaj się sam). W systemie zarządzania flotą pojazdy i kierowcy będą mieli podobny związek. W witrynie e-commerce użytkownicy i produkty mogą mieć relacje wiele-do-wielu, jeśli weźmiemy pod uwagę funkcje, takie jak ulubione lub listy życzeń.

W porządku, teraz jak stworzyć tę relację wiele-do-wielu w SQL? Opierając się na naszej wiedzy o tym, jak działa relacja jeden-do-wielu, kuszące może być myślenie, że klucze obce do drugiej tabeli powinny być przechowywane w obu tabelach. Jeśli jednak spróbujemy to zrobić, napotkamy poważne problemy. Spójrz na ten przykład, w którym autorzy książek powinni mieć relację wiele do wielu:

Na pierwszy rzut oka wszystko wygląda dobrze — książki są przyporządkowywane autorom dokładnie na zasadzie wiele do wielu. Ale spójrz uważnie na dane tabeli autorów: książki o numerach 12 i 13 są napisane przez Petera M. (identyfikator autora 2), dlatego nie pozostaje nam nic innego, jak powtórzyć wpisy. Nie tylko tabela autorów ma teraz problemy z integralnością danych (właściwe normalizacja i tak dalej), wartości w kolumnie id są teraz powtarzane. Oznacza to, że w wybranym przez nas projekcie nie może być kolumny klucza podstawowego (ponieważ klucze podstawowe nie mogą mieć zduplikowanych wartości) i wszystko się rozpada.

Oczywiście potrzebujemy nowego sposobu, aby to zrobić, i na szczęście ten problem został już rozwiązany. Ponieważ przechowywanie kluczy obcych bezpośrednio w obu tabelach psuje sprawę, właściwym sposobem tworzenia relacji wiele-do-wielu w RDBMS jest utworzenie tak zwanej „tabeli łączącej”. Pomysł polega zasadniczo na pozostawieniu dwóch oryginalnych tabel w spokoju i utworzeniu trzeciej tabeli, aby zademonstrować mapowanie wiele do wielu.

Powtórzmy nieudany przykład, aby zawierał tabelę łączącą:

Zauważ, że nastąpiły drastyczne zmiany:

  • Liczba kolumn w tabeli autorów jest zmniejszona.
  • Liczba kolumn w tabeli książek jest zmniejszona.
  • Liczba wierszy w tabeli autorów jest zmniejszona, ponieważ nie ma już potrzeby powtarzania.
  • Pojawiła się nowa tabela o nazwie Authors_books, zawierająca informacje o tym, który identyfikator autora jest powiązany z którym identyfikatorem książki. Moglibyśmy nazwać tabelę łączącą w dowolny sposób, ale zgodnie z konwencją jest to wynik prostego połączenia dwóch tabel, które reprezentuje, za pomocą podkreślenia.

Tabela łącząca nie ma klucza podstawowego iw większości przypadków zawiera tylko dwie kolumny — identyfikatory z dwóch tabel. To prawie tak, jakbyśmy usunęli kolumny klucza obcego z naszego wcześniejszego przykładu i wkleili je do nowej tabeli. Ponieważ nie ma klucza podstawowego, może być tyle powtórzeń, ile potrzeba do zarejestrowania wszystkich relacji.

Teraz możemy zobaczyć na własne oczy, jak tabela łączenia wyraźnie wyświetla relacje, ale jak uzyskać do nich dostęp w naszych aplikacjach? Sekret jest powiązany z nazwą — tabela łączenia. To nie jest kurs na temat zapytań SQL, więc nie będę się w to zagłębiał, ale chodzi o to, że jeśli chcesz wszystkie książki określonego autora w jednym, wydajnym zapytaniu, łączysz tabele SQL w tej samej kolejności -> autorzy, autorzy_książek i książki. Tabele Authors i Authors_books są połączone odpowiednio w kolumnach id i Author_id, podczas gdy tabele Authors_books i books są połączone odpowiednio w kolumnach book_id i id.

Wyczerpujące, tak. Ale spójrz na jasną stronę — zakończyliśmy całą niezbędną teorię/prace u podstaw, które musieliśmy wykonać, zanim zmierzymy się z modelami Eloquent. I przypominam, że wszystkie te rzeczy nie są opcjonalne! Nieznajomość projektu bazy danych pozostawi cię na zawsze w krainie elokwentnego zamieszania. Co więcej, cokolwiek Eloquent robi lub próbuje zrobić, doskonale odzwierciedla te szczegóły na poziomie bazy danych, więc łatwo zrozumieć, dlaczego próba nauczenia się Eloquent podczas ucieczki od RDBMS jest daremnym ćwiczeniem.

Tworzenie relacji modelowych w Laravel Eloquent

Wreszcie, po objeździe, który trwał około 70 000 mil, dotarliśmy do punktu, w którym możemy porozmawiać o Eloquent, jego modelach i sposobach ich tworzenia/używania. W poprzedniej części artykułu dowiedzieliśmy się, że wszystko zaczyna się od bazy danych i sposobu modelowania danych. To uświadomiło mi, że powinienem użyć jednego, kompletnego przykładu, w którym rozpoczynam nowy projekt. Jednocześnie chcę, aby ten przykład dotyczył świata rzeczywistego, a nie blogów i autorów lub książek i półek (które też istnieją w świecie rzeczywistym, ale zostały zabite na śmierć).

Wyobraźmy sobie sklep sprzedający miękkie zabawki. Załóżmy również, że otrzymaliśmy dokument wymagań, z którego możemy zidentyfikować w systemie te cztery podmioty: użytkowników, zamówienia, faktury, towary, kategorie, podkategorie i transakcje. Tak, prawdopodobnie wiąże się to z większymi komplikacjami, ale odłóżmy to na bok i skupmy się na tym, jak przechodzimy od dokumentu do aplikacji.

Po zidentyfikowaniu głównych podmiotów w systemie musimy zastanowić się, w jaki sposób odnoszą się one do siebie pod względem relacji z bazami danych, które omówiliśmy do tej pory. Oto te, które przychodzą mi do głowy:

  • Użytkownicy i zamówienia: Jeden do wielu.
  • Zamówienia i faktury: Jeden do jednego. Zdaję sobie sprawę, że ta relacja nie jest okrojona i wysuszona, a w zależności od domeny biznesowej może istnieć relacja jeden do wielu, wiele do jednego lub wiele do wielu. Ale jeśli chodzi o Twój przeciętny, mały sklep internetowy, jedno zamówienie zakończy się tylko jedną fakturą i odwrotnie.
  • Zamówienia i przedmioty: wiele do wielu.
  • Przedmioty i kategorie: Wiele do jednego. Ponownie, nie jest tak w dużych witrynach e-commerce, ale mamy małą operację.
  • Kategorie i podkategorie: od jednego do wielu. Ponownie znajdziesz większość rzeczywistych przykładów, które temu zaprzeczają, ale hej, Eloquent jest wystarczająco trudny, więc nie utrudniajmy modelowania danych!
  • Zamówienia i transakcje: jeden do wielu. Chciałbym również dodać te dwa punkty jako uzasadnienie mojego wyboru: 1) Mogliśmy również dodać związek między Transakcjami a Fakturami. To tylko decyzja dotycząca modelowania danych. 2) Dlaczego jeden do wielu tutaj? Cóż, często zdarza się, że płatność za zamówienie kończy się niepowodzeniem z jakiegoś powodu i udaje się następnym razem. W tym przypadku mamy dwie transakcje utworzone dla tego zamówienia. To, czy chcemy pokazać te nieudane transakcje, czy nie, jest decyzją biznesową, ale zawsze dobrym pomysłem jest przechwytywanie cennych danych.

Czy są jakieś inne relacje? Cóż, wiele innych związków jest możliwych, ale nie są one praktyczne. Na przykład możemy powiedzieć, że użytkownik ma wiele transakcji, więc powinna istnieć między nimi relacja. Należy tutaj zdać sobie sprawę, że istnieje już pośredni związek: użytkownicy -> zamówienia -> transakcje i ogólnie rzecz biorąc, jest wystarczająco dobry, ponieważ RDBMS to bestie w łączeniu tabel. Po drugie, utworzenie tej relacji oznaczałoby dodanie kolumny user_id do tabeli transakcji. Gdybyśmy zrobili to dla każdej możliwej bezpośredniej relacji, dodalibyśmy znacznie więcej obciążenia do bazy danych (w postaci większej ilości miejsca, zwłaszcza jeśli używane są identyfikatory UUID i utrzymywanie indeksów), łącząc cały system. Jasne, jeśli firma twierdzi, że potrzebuje danych o transakcjach w ciągu 1,5 sekundy, możemy zdecydować się na dodanie tej relacji i przyspieszenie (kompromisy, kompromisy…).

A teraz, panie i panowie, nadszedł czas na napisanie właściwego kodu!

Relacje modelu Laravel — prawdziwy przykład z kodem

Następna faza tego artykułu dotyczy ubrudzenia sobie rąk — ale w użyteczny sposób. Wybierzemy te same jednostki bazy danych, co we wcześniejszym przykładzie e-commerce, i zobaczymy, jak modele w Laravel są tworzone i łączone, od razu po zainstalowaniu Laravel!

Oczywiście zakładam, że masz skonfigurowane środowisko programistyczne i wiesz, jak zainstalować i używać Composer do zarządzania zależnościami.

$ composer global require laravel/installer -W
$ laravel new model-relationships-study

Te dwa polecenia konsoli instalują instalator Laravel (część -W służy do aktualizacji, ponieważ miałem już zainstalowaną starszą wersję). A jeśli jesteś ciekawy, w chwili pisania tego tekstu zainstalowana wersja Laravel to 8.5.9. Czy też powinieneś wpadać w panikę i aktualizować? Odradzałbym to, ponieważ nie spodziewam się większych zmian między Laravel 5 a Laravel 8 w kontekście naszej aplikacji. Niektóre rzeczy uległy zmianie i będą miały wpływ na ten artykuł (na przykład fabryki modeli), ale myślę, że będziesz w stanie przenieść kod.

Ponieważ już przemyśleliśmy model danych i ich relacje, część tworzenia modeli będzie trywialna. Zobaczysz też (teraz brzmię jak zdarta płyta!), jak odzwierciedla schemat bazy danych, ponieważ jest od niego w 100% zależny!

Innymi słowy, musimy najpierw utworzyć migracje (i pliki modeli) dla wszystkich modeli, które zostaną zastosowane w bazie danych. Później możemy pracować nad modelami i zająć się relacjami.

Więc od jakiego modelu zaczniemy? Najprostszy i najmniej połączony, oczywiście. W naszym przypadku oznacza to model User. Ponieważ Laravel jest dostarczany z tym modelem (i nie może bez niego działać 🤣), zmodyfikujmy plik migracji, a także wyczyśćmy model, aby odpowiadał naszym prostym potrzebom.

Oto klasa migracji:

class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
        });
    }
}

Ponieważ tak naprawdę nie budujemy projektu, nie musimy wchodzić w hasła, is_active i tak dalej. Nasza tabela użytkowników będzie miała tylko dwie kolumny: id i nazwę użytkownika.

Następnie utwórzmy migrację dla kategorii. Ponieważ Laravel daje nam również wygodę generowania modelu w jednym poleceniu, skorzystamy z tego, choć na razie nie będziemy dotykać pliku modelu.

$ php artisan make:model Category -m
Model created successfully.
Created Migration: 2021_01_26_093326_create_categories_table

A oto klasa migracji:

class CreateCategoriesTable extends Migration
{
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('name');
        });
    }
}

Jeśli jesteś zaskoczony brakiem funkcji down(), nie bądź; w praktyce rzadko używasz go, ponieważ usunięcie kolumny lub tabeli lub zmiana typu kolumny powoduje utratę danych, których nie można odzyskać. Podczas opracowywania zauważysz, że upuszczasz całą bazę danych, a następnie ponownie uruchamiasz migracje. Ale robimy dygresję, więc wróćmy i zajmijmy się następną istotą. Ponieważ podkategorie są bezpośrednio powiązane z kategoriami, myślę, że dobrym pomysłem jest zrobienie tego w następnej kolejności.

$ php artisan make:model SubCategory -m
Model created successfully.
Created Migration: 2021_01_26_140845_create_sub_categories_table

W porządku, teraz uzupełnijmy plik migracji:

class CreateSubCategoriesTable extends Migration
{
    public function up()
    {
        Schema::create('sub_categories', function (Blueprint $table) {
            $table->id();
            $table->string('name');

            $table->unsignedBigInteger('category_id');
            $table->foreign('category_id')
                ->references('id')
                ->on('categories')
                ->onDelete('cascade');
        });
    }
}

Jak widać, dodaliśmy tutaj osobną kolumnę o nazwie id_kategorii, która będzie przechowywać identyfikatory z tabeli kategorii. Brak nagród za zgadywanie, tworzy to relację jeden do wielu na poziomie bazy danych.

Teraz kolej na przedmioty:

$ php artisan make:model Item -m
Model created successfully.
Created Migration: 2021_01_26_141421_create_items_table

I migracja:

class CreateItemsTable extends Migration
{
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description');
            $table->string('type');
            $table->unsignedInteger('price');
            $table->unsignedInteger('quantity_in_stock');

            $table->unsignedBigInteger('sub_category_id');
            $table->foreign('sub_category_id')
                ->references('id')
                ->on('sub_categories')
                ->onDelete('cascade');
        });
    }
}

Jeśli uważasz, że coś powinno być zrobione inaczej, to w porządku. Dwie osoby rzadko wymyślają ten sam schemat i architekturę. Zwróć uwagę na jedną rzecz, która jest swego rodzaju najlepszą praktyką: zapisałem cenę jako liczbę całkowitą.

Czemu?

Cóż, ludzie zdali sobie sprawę, że obsługa podziałów typu float i wszystko było brzydkie i podatne na błędy po stronie bazy danych, więc zaczęli przechowywać cenę w postaci najmniejszej jednostki walutowej. Na przykład, gdybyśmy działali w USD, pole ceny oznaczałoby centy. W całym systemie wartości i obliczenia będą podawane w centach; tylko wtedy, gdy nadejdzie czas wyświetlenia użytkownikowi lub wysłania pliku PDF e-mailem, podzielimy przez 100 i zaokrąglimy. Sprytne, co?

W każdym razie zauważ, że element jest powiązany z podkategorią w relacji wiele do jednego. Jest również powiązany z kategorią . . . pośrednio poprzez swoją podkategorię. Zobaczymy solidne pokazy wszystkich tych ćwiczeń, ale na razie musimy docenić koncepcje i upewnić się, że mamy 100% jasności.

Następny w kolejce jest model Order i jego migracja:

$ php artisan make:model Order -m
Model created successfully.
Created Migration: 2021_01_26_144157_create_orders_table

Ze względu na zwięzłość uwzględnię tylko niektóre ważne pola migracji. Rozumiem przez to, że szczegóły zamówienia mogą zawierać bardzo wiele rzeczy, ale ograniczymy je do kilku, abyśmy mogli skupić się na koncepcji relacji modelowych.

class CreateOrdersTable extends Migration
{
    public function up()
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->string('status');
            $table->unsignedInteger('total_value');
            $table->unsignedInteger('taxes');
            $table->unsignedInteger('shipping_charges');

            $table->unsignedBigInteger('user_id');
            $table->foreign('user_id')
                ->references('id')
                ->on('users')
                ->onDelete('cascade');
        });
    }
}

Wygląda dobrze, ale chwileczkę! Gdzie są pozycje w tym zamówieniu? Jak ustaliliśmy wcześniej, istnieje relacja wiele do wielu między zamówieniami a towarami, więc prosty klucz obcy nie działa. Rozwiązaniem jest tzw. stół łączący lub stół pośredni. Innymi słowy, potrzebujemy tabeli łączącej do przechowywania mapowania wiele-do-wielu między zamówieniami a przedmiotami. Teraz w świecie Laravel istnieje wbudowana konwencja, której przestrzegamy, aby zaoszczędzić czas: jeśli utworzę nową tabelę, używając liczby pojedynczej dwóch nazw tabel, ułożę je w kolejności słownikowej i połączę za pomocą podkreślenia, Laravel automatycznie rozpozna go jako stół łączący.

W naszym przypadku tabela łącząca będzie się nazywać item_order (słowo „item” występuje przed „order” w słowniku). Ponadto, jak wyjaśniono wcześniej, ta tabela łącząca będzie zwykle zawierać tylko dwie kolumny, klucze obce dla każdej tabeli.

Moglibyśmy tutaj stworzyć model + migrację, ale model nigdy nie zostanie użyty, ponieważ jest to bardziej meta. W ten sposób tworzymy nową migrację w Laravel i mówimy jej co jest co.

$ php artisan make:migration create_item_order_table --create="item_order"
Created Migration: 2021_01_27_093127_create_item_order_table

Powoduje to nową migrację, którą zmienimy w następujący sposób:

class CreateItemOrderTable extends Migration
{
    public function up()
    {
        Schema::create('item_order', function (Blueprint $table) {
            $table->unsignedBigInteger('order_id');
            $table->foreign('order_id')
                ->references('id')
                ->on('orders')
                ->onDelete('cascade');
            
            $table->unsignedBigInteger('item_id');
            $table->foreign('item_id')
                ->references('id')
                ->on('items')
                ->onDelete('cascade');    
        });
    }
}

Jak faktycznie uzyskać dostęp do tych relacji za pomocą wywołań metody Eloquent, to temat na później, ale zauważ, że najpierw musimy skrupulatnie ręcznie utworzyć te klucze obce. Bez nich nie ma Eloquent i nie ma „smart” w Laravel. 🙂

Czy już dotarliśmy? Cóż prawie . . .

Mamy jeszcze tylko kilka modeli, o które musimy się martwić. Pierwszym z nich jest tabela faktur i pamiętasz, że zdecydowaliśmy się, aby była to relacja jeden do jednego z zamówieniami.

$ php artisan make:model Invoice -m
Model created successfully.
Created Migration: 2021_01_27_101116_create_invoices_table

We wczesnych częściach tego artykułu widzieliśmy, że jednym ze sposobów wymuszenia relacji jeden do jednego jest uczynienie klucza podstawowego w tabeli potomnej również kluczem obcym. W praktyce mało kto przyjmuje to nadmiernie ostrożne podejście, a ludzie na ogół projektują schemat tak, jak w przypadku relacji jeden-do-wielu. Uważam, że środkowe podejście jest lepsze; po prostu spraw, aby klucz obcy był unikalny i upewniłeś się, że identyfikatory modelu nadrzędnego nie mogą się powtórzyć:

class CreateInvoicesTable extends Migration
{
    public function up()
    {
        Schema::create('invoices', function (Blueprint $table) {
            $table->id();
            $table->timestamp('raised_at')->nullable();
            $table->string('status');
            $table->unsignedInteger('totalAmount');

            $table->unsignedBigInteger('order_id')->unique();
            $table->foreign('order_id')
                ->references('id')
                ->on('orders')
                ->onDelete('cascade')
                ->unique();
        });
    }
}

I tak, po raz kolejny zdaję sobie sprawę, że w tej tabeli faktur wiele brakuje; jednak naszym celem jest sprawdzenie, jak działają relacje modelu, a nie zaprojektowanie całej bazy danych.

Dobra, więc dotarliśmy do punktu, w którym musimy stworzyć ostateczną migrację naszego systemu (mam nadzieję!). Nacisk kładziony jest teraz na model Transakcji, który, jak zdecydowaliśmy wcześniej, jest powiązany z modelem Zamówienia. Nawiasem mówiąc, oto ćwiczenie dla Ciebie: Czy zamiast tego model Transakcja powinien być połączony z modelem Faktura? Dlaczego i dlaczego nie? 🙂

$ php artisan make:model Transaction -m
Model created successfully.
Created Migration: 2021_01_31_145806_create_transactions_table

I migracja:

class CreateTransactionsTable extends Migration
{
    public function up()
    {
        Schema::create('transactions', function (Blueprint $table) {
            $table->id();
            $table->timestamp('executed_at');
            $table->string('status');
            $table->string('payment_mode');
            $table->string('transaction_reference')->nullable();

            $table->unsignedBigInteger('order_id');
            $table->foreign('order_id')
                ->references('id')
                ->on('orders')
                ->onDelete('cascade');
        });
    }
}

Uff! To była ciężka praca. . . uruchommy migracje i zobaczmy, jak sobie radzimy w oczach bazy danych.

$ php artisan migrate:fresh
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (3.45ms)
Migrating: 2021_01_26_093326_create_categories_table
Migrated:  2021_01_26_093326_create_categories_table (2.67ms)
Migrating: 2021_01_26_140845_create_sub_categories_table
Migrated:  2021_01_26_140845_create_sub_categories_table (3.83ms)
Migrating: 2021_01_26_141421_create_items_table
Migrated:  2021_01_26_141421_create_items_table (6.09ms)
Migrating: 2021_01_26_144157_create_orders_table
Migrated:  2021_01_26_144157_create_orders_table (4.60ms)
Migrating: 2021_01_27_093127_create_item_order_table
Migrated:  2021_01_27_093127_create_item_order_table (3.05ms)
Migrating: 2021_01_27_101116_create_invoices_table
Migrated:  2021_01_27_101116_create_invoices_table (3.95ms)
Migrating: 2021_01_31_145806_create_transactions_table
Migrated:  2021_01_31_145806_create_transactions_table (3.54ms)

Chwała niech będzie Panu! 🙏🏻🙏🏻 Wygląda na to, że przeżyliśmy moment próby.

Dzięki temu jesteśmy gotowi, aby przejść do definiowania relacji modelowych! W tym celu musimy wrócić do utworzonej wcześniej listy, nakreślając rodzaj bezpośrednich relacji między modelami (tabelami).

Na początek ustaliliśmy, że istnieje relacja jeden do wielu między użytkownikami a zamówieniami. Możemy to potwierdzić przechodząc do pliku migracji zamówień i widząc tam obecność pola user_id. To pole jest tym, co tworzy relację, ponieważ każda relacja, którą chcemy nawiązać, musi być najpierw honorowana przez bazę danych; reszta (Elokwentna składnia i gdzie napisać jaką funkcję) to czysta formalność.

Innymi słowy, związek już istnieje. Musimy tylko powiedzieć Eloquentowi, aby udostępnił go w czasie wykonywania. Zacznijmy od modelu Order, gdzie deklarujemy przynależność do modelu User:

<?php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class Order extends Model
{
    use HasFactory;

    public function user() {
        return $this->belongsTo(User::class);
    }
}

Składnia musi być Ci znana; deklarujemy funkcję o nazwie user(), która służy do dostępu do użytkownika, który jest właścicielem tego zamówienia (nazwa funkcji może być dowolna; liczy się to, co zwraca). Pomyśl przez chwilę jeszcze raz — gdyby nie było bazy danych ani kluczy obcych, instrukcja taka jak $this->belongsTo byłaby bez znaczenia. Tylko dlatego, że w tabeli zamówień znajduje się klucz obcy, Laravel może użyć tego identyfikatora użytkownika, aby wyszukać użytkownika o tym samym identyfikatorze i zwrócić go. Sam, bez współpracy bazy danych, Laravel nie jest w stanie stworzyć relacji znikąd.

Teraz byłoby również miło móc pisać $user->orders, aby uzyskać dostęp do zamówień użytkownika. Oznacza to, że musimy przejść do modelu User i napisać funkcję dla części „wiele” tej relacji jeden-do-wielu:

<?php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class User extends Model
{
    use HasFactory;
    public $timestamps = false;

    public function orders() {
        return $this->hasMany(Order::class);
    }
}

Tak, mocno zmodyfikowałem domyślny model użytkownika, ponieważ w tym samouczku nie potrzebujemy wszystkich innych funkcji. W każdym razie klasa User ma teraz metodę o nazwie orders(), która mówi, że jeden użytkownik może być powiązany z wieloma zamówieniami. W świecie ORM mówimy, że relacja orders() jest tutaj odwrotnością relacji user(), którą mieliśmy w modelu Order.

Ale chwileczkę! Jak działa ten związek? Mam na myśli to, że na poziomie bazy danych nie ma wielu połączeń wychodzących z tabeli użytkowników do tabeli zamówień.

W rzeczywistości połączenie istnieje i okazuje się, że samo w sobie wystarczy — odwołanie do klucza obcego przechowywane w tabeli zamówień! To znaczy, kiedy mówimy coś w rodzaju $user->orders, Laravel uderza w funkcję orders() i patrząc na nią wie, że w tabeli zamówień znajduje się klucz obcy. Następnie w pewnym sensie wykonuje polecenia SELECT * FROM WHERE user_id = 23 i zwraca wyniki zapytania jako kolekcję. Oczywiście całym celem posiadania ORM jest zapomnienie o SQL, ale nie powinniśmy całkowicie zapominać, że podstawową podstawą jest RDBMS, który uruchamia zapytania SQL.

Następnie przejrzyjmy modele zamówień i faktur, w których mamy relację jeden do jednego:

<?php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class Order extends Model
{
    use HasFactory;
    public $timestamps = false;

    public function user() {
        return $this->belongsTo(User::class);
    }

    public function invoice() {
        return $this->hasOne(Invoice::class);
    }
}

I wzór faktury:

<?php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class Invoice extends Model
{
    use HasFactory;
    public $timestamps = false;

    public function order() {
        return $this->belongsTo(Order::class);
    }
}

Zauważ, że na poziomie bazy danych, jak również prawie na poziomie Eloquent, jest to typowa relacja jeden do wielu; właśnie dodaliśmy kilka kontroli, aby upewnić się, że pozostaje jeden do jednego.

Dochodzimy teraz do innego rodzaju relacji — relacji wiele do wielu między zamówieniami a przedmiotami. Przypomnijmy, że utworzyliśmy już tabelę pośrednią o nazwie item_order, która przechowuje mapowanie między kluczami głównymi. Jeśli tyle zostało zrobione poprawnie, zdefiniowanie relacji i praca z nią jest trywialna. Zgodnie z dokumentacją Laravela, aby zdefiniować relację wiele-do-wielu, twoje metody muszą zwracać instancję należy doMany().

Tak więc w modelu Item:

<?php

namespace AppModels;

use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;

class Item extends Model
{
    use HasFactory;
    public $timestamps = false;

    public function orders() {
        return $this->belongsToMany(Order::class);
    }
}

Co zaskakujące, odwrotna zależność jest prawie identyczna:

class Order extends Model
{
    /* ... other code */
    
    public function items() {
        return $this->belongsToMany(Item::class);
    }
}

I to wszystko! Tak długo, jak poprawnie przestrzegamy konwencji nazewnictwa, Laravel jest w stanie wydedukować mapowania, a także ich lokalizację.

Ponieważ wszystkie trzy podstawowe typy relacji zostały omówione (jeden do jednego, jeden do wielu, wiele do wielu), przestanę opisywać metody dla innych modeli, ponieważ będą one te same linie. Zamiast tego utwórzmy fabryki dla tych modeli, utwórzmy fikcyjne dane i zobaczmy te relacje w akcji!

Jak to zrobimy? Cóż, wybierzmy szybką i nieczystą ścieżkę i wrzućmy wszystko do domyślnego pliku siewnika. Następnie, gdy uruchomimy migracje, uruchomimy również siewnik. Oto jak wygląda mój plik DatabaseSeeder.php:

<?php

namespace DatabaseSeeders;

use IlluminateDatabaseSeeder;
use AppModelsCategory;
use AppModelsSubCategory;
use AppModelsItem;
use AppModelsOrder;
use AppModelsInvoice;
use AppModelsUser;
use Faker;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $faker = FakerFactory::create();

        // Let's make two users
        $user1 = User::create(['name' => $faker->name]);
        $user2 = User::create(['name' => $faker->name]);

        // Create two categories, each having two subcategories
        $category1 = Category::create(['name' => $faker->word]);
        $category2 = Category::create(['name' => $faker->word]);

        $subCategory1 = SubCategory::create(['name' => $faker->word, 'category_id' => $category1->id]);
        $subCategory2 = SubCategory::create(['name' => $faker->word, 'category_id' => $category1->id]);

        $subCategory3 = SubCategory::create(['name' => $faker->word, 'category_id' => $category2->id]);
        $subCategory4 = SubCategory::create(['name' => $faker->word, 'category_id' => $category2->id]);

        // After categories, well, we have items
        // Let's create two items each for sub-category 2 and 4
        $item1 = Item::create([
            'sub_category_id' => 2,
            'name' => $faker->name,
            'description' => $faker->text,
            'type' => $faker->word,
            'price' => $faker->randomNumber(2),
            'quantity_in_stock' => $faker->randomNumber(2),
        ]);

        $item2 = Item::create([
            'sub_category_id' => 2,
            'name' => $faker->name,
            'description' => $faker->text,
            'type' => $faker->word,
            'price' => $faker->randomNumber(3),
            'quantity_in_stock' => $faker->randomNumber(2),
        ]);

        $item3 = Item::create([
            'sub_category_id' => 4,
            'name' => $faker->name,
            'description' => $faker->text,
            'type' => $faker->word,
            'price' => $faker->randomNumber(4),
            'quantity_in_stock' => $faker->randomNumber(2),
        ]);

        $item4 = Item::create([
            'sub_category_id' => 4,
            'name' => $faker->name,
            'description' => $faker->text,
            'type' => $faker->word,
            'price' => $faker->randomNumber(1),
            'quantity_in_stock' => $faker->randomNumber(3),
        ]);

        // Now that we have users and items, let's make user1 place a couple of orders
        $order1 = Order::create([
            'status' => 'confirmed',
            'total_value' => $faker->randomNumber(3),
            'taxes' => $faker->randomNumber(1),
            'shipping_charges' => $faker->randomNumber(2),
            'user_id' => $user1->id
        ]);

        $order2 = Order::create([
            'status' => 'waiting',
            'total_value' => $faker->randomNumber(3),
            'taxes' => $faker->randomNumber(1),
            'shipping_charges' => $faker->randomNumber(2),
            'user_id' => $user1->id
        ]);

        // now, assigning items to orders
        $order1->items()->attach($item1);
        $order1->items()->attach($item2);
        $order1->items()->attach($item3);
        
        $order2->items()->attach($item1);
        $order2->items()->attach($item4);

        // and finally, create invoices
        $invoice1 = Invoice::create([
            'raised_at' => $faker->dateTimeThisMonth(),
            'status' => 'settled',
            'totalAmount' => $faker->randomNumber(3),
            'order_id' => $order1->id,
        ]);
    }
}

A teraz ponownie konfigurujemy bazę danych i umieszczamy ją:

$ php artisan migrate:fresh --seed
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (43.81ms)
Migrating: 2021_01_26_093326_create_categories_table
Migrated:  2021_01_26_093326_create_categories_table (2.20ms)
Migrating: 2021_01_26_140845_create_sub_categories_table
Migrated:  2021_01_26_140845_create_sub_categories_table (4.56ms)
Migrating: 2021_01_26_141421_create_items_table
Migrated:  2021_01_26_141421_create_items_table (5.79ms)
Migrating: 2021_01_26_144157_create_orders_table
Migrated:  2021_01_26_144157_create_orders_table (6.40ms)
Migrating: 2021_01_27_093127_create_item_order_table
Migrated:  2021_01_27_093127_create_item_order_table (4.66ms)
Migrating: 2021_01_27_101116_create_invoices_table
Migrated:  2021_01_27_101116_create_invoices_table (6.70ms)
Migrating: 2021_01_31_145806_create_transactions_table
Migrated:  2021_01_31_145806_create_transactions_table (6.09ms)
Database seeding completed successfully.

W porządku! Teraz jest ostatnia część tego artykułu, w której po prostu uzyskujemy dostęp do tych relacji i potwierdzamy wszystko, czego się do tej pory nauczyliśmy. Będziesz zachwycony wiedząc (mam nadzieję), że będzie to lekka i zabawna sekcja.

A teraz odpalmy najbardziej zabawny komponent Laravel — interaktywną konsolę Tinker!

$ php artisan tinker
Psy Shell v0.10.6 (PHP 8.0.0 — cli) by Justin Hileman
>>>

Dostęp do relacji modelu jeden do jednego w Laravel Eloquent

Okay, więc najpierw przyjrzyjmy się relacji jeden do jednego, którą mamy w naszych modelach zamówień i faktur:

>>> $order = Order::find(1);
[!] Aliasing 'Order' to 'AppModelsOrder' for this Tinker session.
=> AppModelsOrder {#4108
     id: 1,
     status: "confirmed",
     total_value: 320,
     taxes: 5,
     shipping_charges: 12,
     user_id: 1,
   }
>>> $order->invoice
=> AppModelsInvoice {#4004
     id: 1,
     raised_at: "2021-01-21 19:20:31",
     status: "settled",
     totalAmount: 314,
     order_id: 1,
   }

Zauważ coś? Pamiętaj, że sposób, w jaki zostało to zrobione na poziomie bazy danych, jest to relacja jeden do wielu, gdyby nie dodatkowe ograniczenia. Tak więc Laravel mógł w rezultacie zwrócić kolekcję obiektów (lub tylko jeden obiekt), co byłoby technicznie poprawne. ALE . . . powiedzieliśmy Laravelowi, że jest to relacja jeden do jednego, więc rezultatem jest pojedyncza instancja Eloquent. Zwróć uwagę, jak to samo dzieje się podczas uzyskiwania dostępu do relacji odwrotnej:

$invoice = Invoice::find(1);
[!] Aliasing 'Invoice' to 'AppModelsInvoice' for this Tinker session.
=> AppModelsInvoice {#3319
     id: 1,
     raised_at: "2021-01-21 19:20:31",
     status: "settled",
     totalAmount: 314,
     order_id: 1,
   }
>>> $invoice->order
=> AppModelsOrder {#4042
     id: 1,
     status: "confirmed",
     total_value: 320,
     taxes: 5,
     shipping_charges: 12,
     user_id: 1,
   }

Dostęp do relacji modelowych jeden-do-wielu w Laravel Eloquent

Mamy relację jeden-do-wielu między użytkownikami a zamówieniami. „Majstrujemy” przy tym teraz i zobaczmy wynik:

>>> User::find(1)->orders;
[!] Aliasing 'User' to 'AppModelsUser' for this Tinker session.
=> IlluminateDatabaseEloquentCollection {#4291
     all: [
       AppModelsOrder {#4284
         id: 1,
         status: "confirmed",
         total_value: 320,
         taxes: 5,
         shipping_charges: 12,
         user_id: 1,
       },
       AppModelsOrder {#4280
         id: 2,
         status: "waiting",
         total_value: 713,
         taxes: 4,
         shipping_charges: 80,
         user_id: 1,
       },
     ],
   }
>>> Order::find(1)->user
=> AppModelsUser {#4281
     id: 1,
     name: "Dallas Kshlerin",
   }

Dokładnie tak, jak oczekiwano, dostęp do zamówień użytkownika skutkuje kolekcją rekordów, podczas gdy odwrotność tworzy tylko jeden pojedynczy obiekt User. Innymi słowy, jeden do wielu.

Uzyskiwanie dostępu do relacji modelu wiele-do-wielu w Laravel Eloquent

Przyjrzyjmy się teraz relacji wiele-do-wielu. Mamy jedną taką zależność między towarami a zamówieniami:

>>> $item1 = Item::find(1);
[!] Aliasing 'Item' to 'AppModelsItem' for this Tinker session.
=> AppModelsItem {#4253
     id: 1,
     name: "Russ Kutch",
     description: "Deserunt voluptatibus omnis ut cupiditate doloremque. Perspiciatis officiis odio et accusantium alias aut. Voluptatum provident aut ut et.",
     type: "adipisci",
     price: 26,
     quantity_in_stock: 65,
     sub_category_id: 2,
   }
>>> $order1 = Order::find(1);
=> AppModelsOrder {#4198
     id: 1,
     status: "confirmed",
     total_value: 320,
     taxes: 5,
     shipping_charges: 12,
     user_id: 1,
   }
>>> $order1->items
=> IlluminateDatabaseEloquentCollection {#4255
     all: [
       AppModelsItem {#3636
         id: 1,
         name: "Russ Kutch",
         description: "Deserunt voluptatibus omnis ut cupiditate doloremque. Perspiciatis officiis odio et accusantium alias aut. Voluptatum provident aut ut et.",
         type: "adipisci",
         price: 26,
         quantity_in_stock: 65,
         sub_category_id: 2,
         pivot: IlluminateDatabaseEloquentRelationsPivot {#4264
           order_id: 1,
           item_id: 1,
         },
       },
       AppModelsItem {#3313
         id: 2,
         name: "Mr. Green Cole",
         description: "Maxime beatae porro commodi fugit hic. Et excepturi natus distinctio qui sit qui. Est non non aut necessitatibus aspernatur et aspernatur et. Voluptatem possimus consequatur exercitationem et.",
         type: "pariatur",
         price: 381,
         quantity_in_stock: 82,
         sub_category_id: 2,
         pivot: IlluminateDatabaseEloquentRelationsPivot {#4260
           order_id: 1,
           item_id: 2,
         },
       },
       AppModelsItem {#4265
         id: 3,
         name: "Brianne Weissnat IV",
         description: "Delectus ducimus quia voluptas fuga sed eos esse. Rerum repudiandae incidunt laboriosam. Ea eius omnis autem. Cum pariatur aut voluptas sint aliquam.",
         type: "non",
         price: 3843,
         quantity_in_stock: 26,
         sub_category_id: 4,
         pivot: IlluminateDatabaseEloquentRelationsPivot {#4261
           order_id: 1,
           item_id: 3,
         },
       },
     ],
   }
>>> $item1->orders
=> IlluminateDatabaseEloquentCollection {#4197
     all: [
       AppModelsOrder {#4272
         id: 1,
         status: "confirmed",
         total_value: 320,
         taxes: 5,
         shipping_charges: 12,
         user_id: 1,
         pivot: IlluminateDatabaseEloquentRelationsPivot {#4043
           item_id: 1,
           order_id: 1,
         },
       },
       AppModelsOrder {#4274
         id: 2,
         status: "waiting",
         total_value: 713,
         taxes: 4,
         shipping_charges: 80,
         user_id: 1,
         pivot: IlluminateDatabaseEloquentRelationsPivot {#4257
           item_id: 1,
           order_id: 2,
         },
       },
     ],
   }

Czytanie tego wyniku może przyprawić o zawrót głowy, ale zauważ, że element item1 jest częścią elementów zamówienia 1 i odwrotnie, tak właśnie ustawiamy rzeczy. Zajrzyjmy też do tabeli pośredniej, w której przechowywane są odwzorowania:

>>> use DB;
>>> DB::table('item_order')->select('*')->get();
=> IlluminateSupportCollection {#4290
     all: [
       {#4270
         +"order_id": 1,
         +"item_id": 1,
       },
       {#4276
         +"order_id": 1,
         +"item_id": 2,
       },
       {#4268
         +"order_id": 1,
         +"item_id": 3,
       },
       {#4254
         +"order_id": 2,
         +"item_id": 1,
       },
       {#4267
         +"order_id": 2,
         +"item_id": 4,
       },
     ],
   }

Wniosek

Tak, to jest to, naprawdę! To był naprawdę długi artykuł, ale mam nadzieję, że był przydatny. Czy to wszystko, co trzeba wiedzieć o modelach Laravel?

Niestety nie. Królicza dziura jest naprawdę głęboka i istnieje wiele bardziej wymagających koncepcji, takich jak relacje polimorficzne, dostrajanie wydajności i tak dalej, z którymi spotkasz się, rozwijając się jako programista Laravel. Na razie to, co omówiono w tym artykule, wystarcza 70% programistów na 70% czasu, mniej więcej. Minie naprawdę dużo czasu, zanim poczujesz potrzebę poszerzenia swojej wiedzy.

Mając to zastrzeżenie na uboczu, chcę, abyś zabrał to najważniejsze spostrzeżenie: nic nie jest czarną magią ani nie jest poza zasięgiem w programowaniu. Po prostu nie rozumiemy podstaw i tego, jak rzeczy są zbudowane, co sprawia, że ​​​​walczymy i czujemy się sfrustrowani.

Więc . . . ?

Zainwestuj w siebie! Kursy, książki, artykuły, inne społeczności programistyczne (Python jest moją rekomendacją nr 1) — korzystaj z wszelkich dostępnych zasobów i korzystaj z nich regularnie, choć powoli. Wkrótce liczba przypadków, w których prawdopodobnie schrzanisz całość, drastycznie się zmniejszy.

Dobra, dość kazań. Miłego dnia! 🙂