Förstå modellrelationer i Laravel Eloquent

By rik

Utforska Relationer mellan Modeller i Laravel Eloquent

Modellernas interaktioner utgör kärnan i Laravel Eloquent. Om du känner att dessa relationer är svåra att förstå, eller om du letar efter en tydlig, enkel och komplett guide, då har du kommit rätt!

Som skribent är det lätt att uppträda som en expert, men jag ska vara ärlig – jag hade stora svårigheter med att lära mig Laravel, speciellt eftersom det var mitt första fullstackramverk. En anledning var att jag utforskade det på egen hand, utanför jobbet, vilket ledde till att jag testade, blev förvirrad, gav upp och glömde bort det. Jag upprepade detta mönster 5-6 gånger innan det började kännas logiskt. Och låt oss vara ärliga, dokumentationen hjälper inte alltid.

Det som särskilt förblev oklart var Eloquent, eller åtminstone modellrelationerna (eftersom Eloquent är för omfattande för att lära sig allt på en gång). De typiska exemplen med författare och blogginlägg är inte tillräckligt komplexa för verkliga projekt, och tyvärr använder den officiella dokumentationen liknande exempel. Även när jag hittade användbara artiklar var förklaringarna ofta så bristfälliga att det blev meningslöst att försöka förstå.

(Och om du vill kritisera min kritik av den officiella dokumentationen, vill jag bara påpeka att du kan kolla in Django-dokumentationen först, innan du säger något.)

Med tiden föll allt på plats. Jag lärde mig att modellera projekt ordentligt och arbeta bekvämt med modellerna. Och en dag upptäckte jag några smarta sätt att göra hela processen mer smidig. I den här artikeln tänker jag gå igenom allt, från grunderna till de olika användningsfall som du kommer att stöta på i verkliga projekt.

Varför är Modellrelationer i Eloquent Utmanande?

Jag möter alltför många Laravel-utvecklare som har svårt att förstå modeller på djupet.

Men varför är det så?

Även nu, med alla kurser, artiklar och videor om Laravel, är den generella förståelsen ofta bristfällig. Jag anser att detta är en viktig fråga som förtjänar lite reflektion.

Om du frågar mig, så anser jag inte att modellrelationer i Eloquent är särskilt svåra. Åtminstone inte om vi använder ordet ”svårt” i dess rätta bemärkelse. Migreringar i livesystem är svårt, att skriva en ny mallmotor är svårt och att bidra med kod till Laravel-kärnan är svårt. Jämfört med det, är det väl inte särskilt svårt att lära sig en ORM? 🤭🤭

Vad som händer är att PHP-utvecklare som lär sig Laravel tycker att Eloquent är svårt, det är det egentliga problemet. Och jag tror att det beror på flera saker (varning, här kommer några kontroversiella åsikter):

  • Innan Laravel var många PHP-utvecklare mest bekanta med CodeIgniter (som för övrigt fortfarande lever, även om det blivit mer likt Laravel/CakePHP). I den äldre CodeIgniter-gemenskapen var det ”bästa praxis” att skriva SQL-frågor direkt där de behövdes. Och även om det finns en ny CodeIgniter idag, har dessa vanor hängt kvar. Som ett resultat är konceptet med en ORM helt nytt för många PHP-utvecklare som börjar med Laravel.
  • Om vi bortser från den lilla andel PHP-utvecklare som arbetat med ramverk som Yii, CakePHP, etc., är resten vana vid att arbeta med grundläggande PHP eller i en miljö som WordPress. Återigen är den objektorienterade programmeringen inte lika självklar, och ramverk, tjänstecontainrar, designmönster, ORM… alla dessa koncept känns främmande.
  • Det finns inte någon utbredd kultur för kontinuerligt lärande i PHP-världen. Den genomsnittliga utvecklaren är nöjd med att arbeta med enkla serverkonfigurationer, relationsdatabaser och SQL-frågor skrivna som strängar. Asynkron programmering, webbsockets, HTTP 2/3, Linux (glöm Docker), enhetstester, domändriven design – allt detta är okända idéer för en stor del av PHP-utvecklarna. Detta innebär att många inte är bekväma med att läsa om nya, utmanande tekniker och tycker därför att Eloquent är svårt.
  • Den allmänna förståelsen av databaser och modellering är också bristfällig. Eftersom databasdesign är tätt kopplad till Eloquent-modeller, gör det inlärningsprocessen ännu svårare.

Jag vill inte låta hård eller generaliserande – det finns många utmärkta PHP-utvecklare, men de utgör en liten andel.

Om du läser det här, så har du troligtvis övervunnit dessa utmaningar, stött på Laravel och kämpar med Eloquent.

Grattis! 👏

Du är nästan framme. Alla bitar är på plats, vi behöver bara gå igenom dem i rätt ordning. Med andra ord, låt oss börja med databasnivån.

Databasmodeller: Relationer och Kardinalitet

För att göra det enkelt antar vi att vi bara arbetar med relationsdatabaser i den här artikeln. En anledning är att ORM:er utvecklades för relationsdatabaser, och den andra är att RDBMS fortfarande är mycket populära.

Datamodell

Låt oss börja med att förstå datamodeller. En datamodell (eller bara en modell) kommer från databasen. Ingen databas, ingen data, ingen datamodell. Vad är då en datamodell? Det är helt enkelt sättet du väljer att lagra och strukturera din data. Till exempel, i en e-handel kan du lagra allt i en enda enorm tabell (en hemsk praxis, men tyvärr inte ovanligt i PHP-världen). Det skulle vara din datamodell. Du kan också dela upp datan i 20 huvudtabeller och 16 kopplingstabeller – det är också en datamodell.

Observera också att hur data är strukturerad i databasen inte behöver matcha exakt hur den är organiserad i ramverkets ORM. Men vi strävar alltid efter att hålla dessa så lika som möjligt, så att det inte blir ytterligare en sak att tänka på när vi utvecklar.

Kardinalitet

Låt oss prata om kardinalitet. Det syftar helt enkelt på ”antal”, till exempel 1, 2, 3… Det är allt. Nu kan vi gå vidare!

Relationer

När vi lagrar data i ett system kan datapunkter vara relaterade till varandra. Jag vet att detta låter abstrakt, men häng med lite till. Sättet som dataobjekt är sammankopplade på kallas relationer. Låt oss titta på några exempel utan databaser, så att vi är säkra på att vi förstår konceptet.

  • Om vi lagrar allt i en array, kan relationen vara: nästa dataobjekt har ett index som är 1 högre än det föregående.
  • Om vi lagrar data i ett binärt träd, kan en relation vara att vänster barnnod alltid har ett mindre värde än sin föräldranod (om vi valt att organisera trädet på det sättet).
  • Om vi lagrar data som en array av arrayer med samma längd, kan vi simulera en matris och dess egenskaper blir relationerna för vår data.

Vi ser alltså att ordet ”relation” inte har en fast definition i datasammanhang. Om två personer tittade på samma data, kan de identifiera olika relationer (hej statistik!) och båda kan vara korrekta.

Relationsdatabaser

Med alla dessa termer i åtanke, kan vi nu prata om något som är direkt kopplat till modeller i ett webbramverk (Laravel) – relationsdatabaser. De flesta av oss använder MySQL, MariaDB, PostgreSQL, MSSQL, SQL Server, SQLite eller liknande. Vi vet att dessa kallas RDBMS, men många har glömt vad det egentligen betyder.

”R” i RDBMS står för Relational. Detta är inte en slumpmässig term, utan det lyfter fram att dessa databassystem är utformade för att effektivt hantera relationer mellan lagrad data. I själva verket har ”relation” en specifik matematisk betydelse, och även om ingen utvecklare behöver fördjupa sig i det, är det bra att veta att det finns en rigorös matematisk grund för den här typen av databaser.

Utforska dessa resurser för att lära dig mer om SQL och NoSQL.

Vi vet att data lagras i RDBMS som tabeller. Men var finns relationerna?

Typer av Relationer i RDBMS

Detta är kanske det viktigaste i hela ämnet om Laravel och modellrelationer. Om du inte förstår detta kommer Eloquent aldrig att kännas logiskt. Så var uppmärksam de närmaste minuterna (det är inte så svårt!).

RDBMS tillåter oss att ha relationer mellan data – på databasnivå. Dessa relationer är inte godtyckliga, utan kan skapas och tolkas på samma sätt av olika personer.

Det finns vissa verktyg och funktioner i ett RDBMS som hjälper oss att skapa och upprätthålla dessa relationer, till exempel:

  • Primärnyckel
  • Främmande nyckel
  • Begränsningar

Jag vill inte göra den här artikeln till en databaskurs, så jag antar att du känner till dessa begrepp. Om inte, eller om du är osäker, rekommenderar jag den här videon (och hela serien):

De relationer som finns i RDBMS är också de vanligaste i verkliga applikationer (inte alltid, eftersom till exempel ett socialt nätverk modelleras bäst som en graf snarare än tabeller). Låt oss gå igenom dem och se var de kan vara användbara.

En-till-En Relation

Nästan alla webbapplikationer har användarkonton. Följande är också sant (i allmänhet) om användare och konton:

  • En användare kan bara ha ett konto.
  • Ett konto kan bara ägas av en användare.

Visst, en person kan registrera sig med en annan e-postadress och skapa två konton, men i applikationens perspektiv är det två olika användare med två olika konton. Applikationen visar till exempel inte ett kontos data i ett annat konto.

Detta betyder att om du har en sådan situation i din applikation, och du använder en relationsdatabas, måste du designa det som en en-till-en relation. Ingen tvingar dig – det finns en tydlig situation i affärsdomänen och du använder en relationsdatabas. När båda dessa villkor uppfylls har du en en-till-en relation.

För exemplet med användare och konton, kan vi implementera relationen på detta sätt:

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)
);

Ser du tricket? Det är ganska ovanligt att i account-tabellen använda id som både primärnyckel och främmande nyckel! Den främmande nyckeln länkar till användartabellen, medan primärnyckeln gör id-kolumnen unik – en sann en-till-en relation!

Visserligen är inte denna relation helt garanterad. Vi kan till exempel lägga till 200 nya användare utan att lägga till en enda post i account-tabellen. Då får vi en en-till-noll relation! 🤭🤭 Men rent strukturmässigt är det det bästa vi kan göra. För att förhindra att användare skapas utan konton, behöver vi lägga till logik, antingen databasutlösare eller valideringar i Laravel.

Om du känner dig stressad, så har jag några råd:

  • Ta det lugnt. Ta det så långsamt du behöver. Istället för att stressa genom den här artikeln och 15 andra, fokusera på den här. Låt det ta 3, 4, 5 dagar om det behövs. Målet är att förstå modellrelationer i Eloquent en gång för alla. Du har tidigare hoppat mellan artiklar, slösat bort massor av tid, och det har ändå inte fungerat. Så gör något annorlunda den här gången. 😇
  • Den här artikeln handlar om Laravel Eloquent, men det viktigaste är databasschemat. Så fokusera på att få det rätt först. Om du inte kan jobba på databasnivå (utan att tänka på ramverk), kommer modeller och relationer aldrig att kännas helt logiska. Så glöm Laravel för stunden. Vi fokuserar bara på databasdesign. Jag kommer att nämna Laravel ibland, men ignorera det om det gör saker mer komplicerade.
  • Läs mer om databaser. Index, prestanda, triggers, underliggande datastrukturer, cachning, relationer i MongoDB… alla relaterade ämnen kommer att hjälpa dig. Kom ihåg att ramverksmodeller bara är ett skal, och att den riktiga funktionaliteten kommer från de underliggande databaserna.

En-till-Många Relation

Du kanske redan har märkt det, men den här typen av relation skapar vi intuitivt i vårt arbete. När vi skapar en order-tabell och lägger till en främmande nyckel till användartabellen, skapar vi en en-till-många relation mellan användare och order. En användare kan ha flera order, vilket är vanligt i e-handel. Och en order kan bara tillhöra en användare.

I datamodellering representeras en sådan situation schematiskt så här:

Ser du de tre linjerna som bildar en treudd? Det symboliserar ”många”. Diagrammet visar att en användare kan ha många order.

Detta ”många” och ”en” är kardinaliteten i en relation. För den här artikeln är det inte så viktigt, men det är bra att känna till om det dyker upp i intervjuer eller annan läsning.

Enkelt, eller hur? Och det är lika enkelt att skapa den här relationen i SQL, faktiskt mycket enklare än en en-till-en relation!

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) 
);

Order-tabellen lagrar användar-id för varje order. Det finns ingen begränsning att användar-id måste vara unika i ordertabellen, vilket innebär att samma id kan upprepas. Det är detta som skapar en-till-många relationen, inte någon magi. Användar-id lagras i order-tabellen, och SQL har inte något koncept för en-till-många. Men när vi lagrar data på det här sättet, kan vi se det som en en-till-många relation.

Förhoppningsvis känns det mer logiskt nu. Kom ihåg att det handlar om övning. Efter att ha gjort detta några gånger kommer du inte ens att tänka på det.

Många-till-Många Relationer

Nästa relation är många-till-många. Låt oss först tänka på en analogi: böcker och författare. En författare har skrivit flera böcker, och många författare kan samarbeta om en bok. En författare kan skriva många böcker, och många författare kan skriva en bok. Det är en många-till-många relation.

Låt oss ta fler exempel. I ett B2B-system beställer en tillverkare varor från en leverantör och får en faktura. Fakturan har flera rader som visar antal och artikel, t.ex. ”200 rörbitar”. Artiklarna och fakturorna har en många-till-många relation. I ett system för fordonshantering har fordon och förare en liknande relation. I en e-handel kan användare och produkter ha en många-till-många relation, om vi tänker på funktioner som favoriter eller önskelistor.

Hur skapar vi nu en många-till-många relation i SQL? Det kan vara frestande att lagra främmande nycklar i båda tabellerna, men då får vi problem. Ta det här exemplet där böcker och författare ska ha en många-till-många relation:

Till en början ser det bra ut – böcker kopplas till författare på ett många-till-många sätt. Men titta på tabellen med författare: bok-id 12 och 13 är båda skrivna av Peter M., så vi upprepar posterna. Förutom att tabellen nu har problem med dataintegritet (och normalisering), så upprepas även värden i id-kolumnen. Det betyder att den valda designen inte kan ha en primärnyckel (primärnycklar kan inte ha dubblettvärden), och allting faller samman.

Vi behöver ett annat sätt. Lösningen är att skapa en ”kopplingstabell”. Tanken är att lämna de ursprungliga tabellerna ostörda och skapa en tredje tabell som visar relationen.

Låt oss göra om det misslyckade exemplet med en kopplingstabell:

Nu ser vi att:

  • Antalet kolumner i tabellen med författare har minskat.
  • Antalet kolumner i tabellen med böcker har minskat.
  • Antalet rader i författartabellen minskar, eftersom de inte behöver upprepas.
  • En ny tabell authors_books har dykt upp, som innehåller information om vilket författar-id som är kopplat till vilket bok-id. Vi kunde ha döpt tabellen vad som helst, men det är praxis att använda de två tabellerna i kombination med ett understreck.

Kopplingstabellen har ingen primärnyckel och innehåller oftast bara två kolumner: id från de två tabellerna. Vi har i stort sett tagit bort de främmande nycklarna från det tidigare exemplet och klistrat in dem i den här tabellen. Eftersom det inte finns någon primärnyckel, kan samma relationer upprepas flera gånger.

Nu ser vi hur kopplingstabellen visar relationerna, men hur når vi dem i våra applikationer? Hemligheten ligger i namnet – kopplingstabell. Det här är ingen SQL-kurs, men tanken är att om du vill ha alla böcker av en författare, kopplar du tabellerna i denna ordning: författare, authors_books och böcker. Tabellerna författare och authors_books kopplas via id- och author_id-kolumnerna, medan authors_books och books kopplas på book_id- och id-kolumnerna.

Utröttande, ja. Men vi har nu gått igenom all teori vi behöver, innan vi tar oss an modellerna i Eloquent. Och glöm inte att det här inte är frivilligt! Utan kunskap om databasdesign kommer du aldrig att förstå Eloquent. Vad Eloquent gör, återspeglar dessa databasdetaljer, så det är meningslöst att försöka lära sig Eloquent utan att förstå RDBMS.

Skapa Modellrelationer i Laravel Eloquent

Efter en lång omväg har vi äntligen kommit till punkten där vi kan prata om Eloquent, dess modeller och hur man skapar dem. Vi vet att allt börjar med databasen och hur vi modellerar vår data. Vi bör använda ett komplett exempel där vi startar ett nytt projekt, men det ska inte vara ett exempel med bloggar och författare.

Låt oss föreställa oss en butik som säljer mjukisdjur. Vi har fått ett kravdokument som nämner följande enheter: användare, beställningar, fakturor, artiklar, kategorier, underkategorier och transaktioner. Visst, det kan finnas fler komplikationer, men låt oss fokusera på att gå från dokument till en app.

När vi identifierat de viktigaste enheterna, måste vi fundera på hur de relaterar till varandra, utifrån de databasrelationer vi diskuterat. Här är de relationer jag ser:

  • Användare och beställningar: En till många.
  • Beställningar och fakturor: En till en. Det kan finnas andra lösningar, men i en liten e-handelsbutik leder en order oftast till en faktura.
  • Beställningar och varor: Många till många.
  • Artiklar och kategorier: Många till en.
  • Kategorier och underkategorier: En till många.
  • Beställningar och transaktioner: En till många. Vi kan även lägga till en relation mellan transaktioner och fakturor, det är bara ett val av datamodell. Och en orderbetalning kan misslyckas och lyckas senare, så vi har två transaktioner per order. Huruvida vi visar misslyckade transaktioner är ett affärsbeslut, men det är alltid bra att samla data.

Finns det några andra relationer? Många relationer är möjliga, men inte praktiska. En användare kan ha flera transaktioner, men det finns redan en indirekt relation: användare -> order -> transaktioner. Det är oftast tillräckligt, eftersom RDBMS är duktiga på att koppla tabeller. Att skapa en direkt relation skulle kräva att vi lägger till en user_id kolumn i transaktionstabellen. Om vi gjorde det för alla direkta relationer, skulle vi lägga mer belastning på databasen (lagring, index). Om företaget däremot behöver transaktionsdata på 1,5 sekunder, kan vi lägga till relationen (det handlar om att väga för- och nackdelar).

Nu är det dags att skriva kod!

Laravel Modellrelationer – Ett Exempel med Kod

I den här delen går vi in på det praktiska. Vi kommer att använda samma databasenheter som i e-handelsexemplet, och se hur modeller i Laravel skapas och kopplas, från en ny Laravel-installation!

Jag antar att du har din utvecklingsmiljö redo och att du vet hur man installerar och använder Composer.

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

Dessa kommandon installerar Laravel-installationsprogrammet. Jag använder -W för att uppgradera (jag hade en äldre version). Laravel-versionen som installerades var 8.5.9. Behöver du uppgradera? Inte nödvändigt, jag förväntar mig inga stora skillnader mellan Laravel 5 och 8. Vissa saker har förändrats (t.ex. Model Factory), men du bör kunna anpassa koden.

Eftersom vi redan tänkt igenom datamodellen och relationerna, kommer skapandet av modellerna att vara enkelt. Och du kommer att se (jag låter som en trasig skiva) hur det speglar databasschemat, eftersom det är helt beroende av det!

Vi måste först skapa migreringarna (och modellfilerna) som tillämpas på databasen. Sedan kan vi jobba med modellerna och relationerna.

Vilken modell börjar vi med? Den enklaste, förstås. I vårt fall är det användarmodellen. Laravel levereras med den, så vi kan ändra migreringsfilen och rensa upp modellen för att passa våra enkla behov.

Här är migreringsklassen:

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

Vi behöver inte lösenord eller liknande, vår användartabell kommer bara ha två kolumner: id och användarens namn.

Låt oss skapa migreringen för kategori. Vi kan generera modellen med samma kommando:

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

Här är migreringsklassen:

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

Om du saknar down()-funktionen, så är det inte så konstigt. I praktiken tar vi sällan bort kolumner, eftersom det leder till dataförlust. Under utvecklingen släpper vi hela databasen och kör om migreringarna. Men låt oss återgå till nästa enhet. Underkategorier är relaterade till kategorier, så det känns logiskt att göra det härnäst.

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

Okej, låt oss fylla i migreringsfilen:

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');
        });
    }
}

Vi lägger till en kolumn category_id, som lagrar id från kategoritabellen. Det skapar en en-till-många relation på databasnivå.

Nu tar vi tag i artiklarna:

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

Och migreringen:

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');
        });
    }
}

Om du känner att något borde göras annorlunda, så är det bra. Två personer kommer sällan fram till exakt samma schema. Jag har till exempel lagrat priset som ett heltal.

Varför?

Att hantera flyttal är komplicerat på databassidan, så vi lagrar priset i den minsta valutaenheten. Om vi jobbar i USD är prisfältet cent. Genom hela systemet är värden och beräkningar i cent. Först när vi ska visa det för användaren, dividerar vi med 100 och avrundar. Smart, eller hur?

Artikeln är kopplad till en underkategori i en många-till-en relation. Den är även länkad till en kategori, indirekt via underkategorin. Vi kommer att se det i praktiken senare, men låt oss fokusera på att förstå koncept