Vaak komt er een punt in het leven van elke developer waar je interactie nodig hebt met een database. Op dit vlak maakt Eloquent, de Object Relational Mapper (ORM) van Laravel, het proces van interactie met je databasetabellen intuïtief en natuurlijk.

Het is van vitaal belang dat je als professional de zes belangrijkste relatietypes herkent en begrijpt. En je hebt geluk, want we zullen we allemaal doornemen en bespreken.

Wat zijn relaties in Eloquent?

Bij het werken met tabellen in een relationele database kunnen we relaties karakteriseren als verbindingen tussen tabellen. Ze helpen je om gegevens moeiteloos te organiseren en te structureren, wat zorgt voor een superieure leesbaarheid en verwerking van gegevens. In de praktijk zijn er drie soorten databaserelaties:

  • one-to-one – Eén record in een tabel is geassocieerd met één, en slechts één, in een andere tabel. Bijvoorbeeld een persoon en een burgerservicenummer.
  • one-to-many – Een record is gekoppeld aan meerdere records in een andere tabel. Bijvoorbeeld een schrijver en zijn blogs.
  • many-to-many – Meerdere records in een tabel zijn gekoppeld aan meerdere records in een andere tabel. Bijvoorbeeld studenten en de cursussen waarvoor ze zijn ingeschreven.

Laravel maakt het naadloos om databaserelaties op te zetten en te beheren met behulp van objectgeoriënteerde syntaxis in Eloquent.

Naast deze definities introduceert Laravel nog meer relaties, namelijk:

  • Has-many through
  • Polymorphic relaties
  • Many-to-many polymorph

Neem bijvoorbeeld een winkel waarvan de voorraad een verscheidenheid aan artikelen bevat, elk in een eigen categorie. Het is het vanuit zakelijk oogpunt zinvol om de database op te splitsen in meerdere tabellen. Dit geeft zijn eigen problemen, omdat je niet elke afzonderlijke tabel wilt query’en.

We kunnen eenvoudig een one-to-many relatie maken in Laravel om ons te helpen, bijvoorbeeld als we de producten moeten query’en, kunnen we dat doen door het Product model te gebruiken.

Database schema met drie tabellen en een gezamenlijke tabel die een polymorfe relatie vertegenwoordigt
Database schema met drie tabellen en een gezamenlijke tabel die een polymorphic relatie vertegenwoordigt

One-to-one relatie

Dit is eerste basisrelatie die Laravel aanbiedt. Hierbij worden twee tabellen op zo’n manier geassocieerd dat een rij uit de eerste tabel is gecorreleerd met slechts één rij uit de andere tabel.

Om dit in actie te zien, moeten we twee modellen maken met hun eigen migratie:

php artisan make:model Tenant 
Php artisan make:model Rent

We hebben nu twee modellen, waarvan de ene de Tenant is en de andere de Rent.

<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class Tenant extends Model
{
    /**
    * Get the rent of a Tenant
    */
    public function rent() 
    {
        return $this->hasOne(Rent::class);
    }
}

Omdat Eloquent de foreign key relatie bepaalt op basis van de naam van het bovenliggende model (Tenant in dit geval), gaat het Rent model ervan uit dat er een tenant_id foreign key bestaat.

We kunnen dit eenvoudig overschrijven met een extra argument voor de hasOne methode:

return $this- >hasOne(Rent::class, "custom_key");

Eloquent gaat er ook van uit dat er een overeenkomst is tussen de gedefinieerde foreign key en de primary key van de parent (Tenant model). Standaard wordt gekeken of tenant_id overeenkomt met de id sleutel van het Tenant record. We kunnen dit overschrijven met een derde argument in de hasOne methode, zodat het overeenkomt met een andere key:

return $this->hasOne(Rent::class, "custom_key", "other_key"); 

Nu we de one-to-one relatie tussen de modellen hebben gedefinieerd, kunnen we deze eenvoudig gebruiken, zoals dit:

$rent = Tenant::find(10)->rent;

Met deze regel code krijgen we de huur van de huurder met id 10 als die bestaat.

One-to-many relatie

Net als de vorige relatie definiëren we hier relaties tussen een model met één parent en meerdere modellen met children. Het is onwaarschijnlijk dat onze huurder (tenant) maar één keer huur (rent) heeft overgemaaktheeft, omdat het een terugkerende betaling is.

In dit geval heeft onze vorige relatie fouten en die kunnen we herstellen:

<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class Tenant extends Model
{
    /**
    * Get the rents of a Tenant
    */
    public function rent() 
    {
        return $this->hasMany(Rent::class);
    }
}

Voordat we de methode callen om de huren te krijgen, is het goed om te weten dat relaties dienen als querybouwers, dus we kunnen verdere beperkingen toevoegen (zoals huur tussen twee data, minimale betaling, etc.) en ze aan elkaar koppelen om ons gewenste resultaat te krijgen:

$rents = Tenant::find(10)->rent()->where('payment', '>', 500)->first();

En net als bij de vorige relatie kunnen we de foreign en local keys overschrijven door extra argumenten door te geven:

return $this->hasMany(Rent::class, "foreign_key");
return $this->hasMany(Rent::class, "foreign_key", "local_key");

Nu hebben we alle huur van een huurder, maar wat doen we als we de huur weten en willen uitzoeken aan wie deze toebehoort? We kunnen gebruik maken van de property belongsTo :

<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class Rent extends Model
{
    /**
    * Return the tenant for the rent
    */
    public function tenant() 
    {
        return $this->belongsTo(Tenant::class);
    }
}

En nu kunnen we de huurder gemakkelijk achterhalen:

$tenant = Rent::find(1)->tenant;

Voor de belongsTo methode kunnen we ook de foreign en local keys overschrijven zoals we eerder deden.

Has-one-of-many

Aangezien ons Tenant model kan worden geassocieerd met veel Tenant modellen, willen we gemakkelijk het laatste of oudste gerelateerde model van de relaties ophalen.

Een handige manier om dit te doen is door de methoden hasOne en ofMany te combineren:

public function latestRent() {
    return $this->hasOne(Rent::class)->latestOfMany();
}

public function oldestRent() {
    return $this->hasOne(Rent::class)->oldestOfMany();
}

Standaard krijgen we de gegevens op basis van de primary key, die sorteerbaar is, maar we kunnen onze eigen filters maken voor de ofMany methode:

return $this->hasOne(Rent::class)->ofMany('price', 'min');

HasOneThrough en HasManyThrough relaties

De -Through methodes suggereren dat onze modellen door een ander model moeten gaan om een relatie te leggen met het gewenste model. We kunnen bijvoorbeeld de Rent koppelen aan de Landlord, maar de Rent moet eerst door de Tenant om bij de Landlord te komen.

De keys van de tabellen die hiervoor nodig zijn, zien er als volgt uit:

rent
    id - integer
    name - string
    value - double

tenants
    id - integer
    name - string
    rent_id - integer

landlord
    id - integer
    name - string
    tenant_id - integer

Nadat we hebben gevisualiseerd hoe onze tabellen eruit zien, kunnen we de modellen maken:

<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class Rent extends Model
{
    /**
    * Return the rents' landlord
    */
    public function rentLandlord() 
    {
        return $this->hasOneThrough(Landlord::class, Tenant::class);
    }
}

Het eerste argument van de methode hasOneThrough is het model dat je wilt benaderen, en het tweede argument is het model waar je doorheen gaat.

En net als eerder kun je de foreign en local keys overschrijven. Nu we twee modellen hebben, hebben we er twee van elk om te overschrijven in deze volgorde:

public function rentLandlord() 
{
    return $this->hasOneThrough(
        Landlord::class,
        Tenant::class,
        "rent_id",    // Foreign key on the tenant table
        "tenant_id",  // Foreign key on the landlord table
        "id",         // Local key on the tenant class
        "id"          // Local key on the tenant table
    );
}

Op dezelfde manier is de “Has Many Through” relatie in Laravel Eloquent handig als je records in een distant tabel wilt benaderen via een intermediate tabel. Laten we eens kijken naar een voorbeeld met drie tabellen:

  • country
  • user
  • games

Elk land (country) heeft veel gebruikers (users) en elke gebruiker heeft veel spellen (games). We willen alle spellen van een land ophalen via de User tabel.

Je zou de tabellen als volgt definiëren:

country
    id - integer
    name - string

user
    id - integer
    country_id - integer
    name - string

games
    id - integer
    user_id - integer
    title - string

Nu moet je voor elke tabel het Eloquent model definiëren:

<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class Country extends Model
{
    protected $fillable = ['name'];

    public function users()
    {
        return $this->hasMany(User::class);
    }

    public function games()
    {
        return $this->hasManyThrough(Games::class, User::class);
    }
}
<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class User extends Model
{
    protected $fillable = [article_id, 'name'];

    public function country()
    {
        return $this->belongsTo(Country::class);
    }

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}
<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class Game extends Model
{
    protected $fillable = ['user_id', 'title'];

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

Nu kunnen we de games() methode van het Country model callen om alle games te krijgen, omdat we de “Has Many Through” relatie tussen Country en Game hebben gelegd via het User model.

<?php

$country = Country::find(159);
            
// Retrieve all games for the country
$games = $country->games;

Many-to-many relatie

De many-to-many relatie is ingewikkelder. Een goed voorbeeld is een medewerker (employee) die meerdere rollen heeft. Ook een rol kan worden toegewezen aan meerdere medewerkers. Dit is de basis van de many-to-many relatie.

Hiervoor hebben we de tabellen employees, roles en role_employees nodig.

De tabelstructuur van onze database ziet er als volgt uit:

employees
    id - integer
    name - string

roles 
    id - integer
    name - string

role_employees
    user_id - integer
    role_id - integer

Als we de tabellenstructuur van de relatie kennen, kunnen we eenvoudig ons Employee model definiëren als belongToMany Role model.

<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class Employee extends Model
{
    public function roles() 
    {
        return $this- >belongsToMany(Role::class);
    }
}

Zodra we dit gedefinieerd hebben, kunnen we alle rollen van een werknemer callen en zelfs filteren:

$employee = Employee::find(1);
$employee->roles->forEach(function($role) { // });

// OR 

$employee = Employee::find(1)->roles()->orderBy('name')->where('name', 'admin')->get();

Net als alle andere methoden kunnen we de foreign en local keys van de belongsToMany methode overschrijven.

Om de omgekeerde relatie van belongsToMany te definiëren, gebruiken we gewoon dezelfde methode, maar nu op de child methode, met de parent als argument.

<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class Role extends Model
{
    public function employees() 
    {
        return $this->belongsToMany(Employee::class);
    }
}

Gebruik van de intermediate tabel

Zoals we misschien hebben gemerkt, moeten we altijd een intermediate hebben als we de many-to-many relatie gebruiken. In dit geval gebruiken we de tabel role_employees.

Standaard zal onze tabel alleen de id attributen bevatten. Als we andere attributen willen, moeten we ze als volgt specificeren:

return $this->belongsToMany(Employee::class)->withPivot("active", "created_at");

Als we de pivot voor de tijdstempels willen verkorten, kunnen we dat doen:

return $this->belongsToMany(Employee::class)->withTimestamps();

Een truc om te weten is dat we de naam ‘pivot’ kunnen aanpassen in iets dat beter past bij onze applicatie:

return $this->belongsToMany(Employee::class)->as('subscription')->withPivot("active", "created_by");

Het filteren van de resultaten van een eloquent query is een must voor elke developer die zijn Laravel applicaties wil optimaliseren.

Daarom biedt Laravel een fantastische functie van pivots die kunnen worden gebruikt om de gegevens te filteren die we willen verzamelen. Dus in plaats van andere functies zoals databasetransacties te gebruiken om onze gegevens in chunks te krijgen, kunnen we ze filteren met handige methoden zoals wherePivot, wherePivotIn, wherePivotNotIn, wherePivotBetween, wherePivotNotBetween, wherePivotNull, wherePivotNotNull en we kunnen ze gebruiken bij het definiëren van relaties tussen tabellen!

return $this->belongsToMany(Employee::class)->wherePivot('promoted', 1);
return $this->belongsToMany(Employee::class)->wherePivotIn('level', [1, 2]);
return $this->belongsToMany(Employee::class)->wherePivotNotIn('level', [2, 3]);
return $this->belongsToMany(Employee::class)->wherePivotBetween('posted_at', ['2023-01-01 00:00:00', '2023-01-02 00:00:00']);
return $this->belongsToMany(Employee::class)->wherePivotNull('expired_at');
return $this->belongsToMany(Employee::class)->wherePivotNotNull('posted_at');

Een laatste geweldige feature is dat we kunnen ordenen op pivots:

return $this->belongsToMany(Employee::class)
        ->where('promoted', true)
        ->orderByPivot('hired_at', 'desc');

Polymorphic relaties

Het woord polymorph (polymorf) komt uit het Grieks en betekent “vele vormen” Zo kan één model in onze applicatie vele vormen aannemen, wat betekent dat het meer dan één associatie kan hebben. Stel je voor dat we een applicatie bouwen met blogs, video’s, polls, enz. Een gebruiker kan voor elk van deze een opmerking maken. Daarom kan een Comment model bij de modellen Blogs, Video’s en Polls horen.

Polymorphic one-to-one

Dit type relatie is vergelijkbaar met een standaard one-to-one relatie. Het enige verschil is dat het child model bij meer dan één type model kan horen met een enkele associatie.

Neem bijvoorbeeld een Tenant en Landlord model, het kan een polymorphic relatie delen met een Waterbills model.

De tabelstructuur kan er als volgt uitzien:

tenants
    id – integer
    name – string

landlords
    id – integer
    name – string

waterbills
    id – integer
    amount – double
    waterbillable_id
    waterbillable_type

We gebruiken waterbillable_id voor de id van de landlord of tenant, terwijl de waterbillable_type de klassenaam van het parent model bevat. De type kolom wordt door eloquent gebruikt om uit te zoeken welk parent model moet worden geretourneerd.

De modeldefinitie voor een dergelijke relatie ziet er als volgt uit:

<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class WaterBill extends Model
{
    public function billable()
    {
        return $this->morphTo();
    }
}

class Tenant extends Model
{
    public function waterBill()    
    {
        return $this->morphOne(WaterBill::class, 'billable');
    }
}

class Landlord extends Model
{
    public function waterBill()    
    {
        return $this->morphOne(WaterBill::class, 'billable');
    }
}

Als we dit allemaal voor elkaar hebben, kunnen we de gegevens van zowel het Landlord als het Tenant model gebruiken:

<?php

$tenant = Tenant::find(1)->waterBill;
$landlord = Landlord::find(1)->waterBill;

Polymorphic one-to-many

Dit is vergelijkbaar met een gewone one-to-many relatie, het enige belangrijke verschil is dat het child model bij meer dan één type model kan horen, met behulp van één associatie.

In een applicatie als Facebook kunnen gebruikers commentaar geven op berichten, video’s, polls, live, enz. Met een polymorphic one-to-many kunnen we een enkele comments tabellen gebruiken om de reacties op te slaan voor alle categorieën die we hebben. Onze tabelstructuur zou er ongeveer zo uitzien:

posts 
    id – integer
    title – string
    body – text

videos
    id – integer
    title – string
    url – string

polls
    id – integer
    title – string

comments 
    id – integer
    body – text
    commentable_id – integer
    commentable_type – string

De commentable_id is de id van de record en de commentable_type is het klasse-type, zodat eloquent weet waar hij naar moet zoeken. De structuur van het model lijkt erg op de polymorphic one-to-many:

<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class Comment extends Model 
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Poll extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Live extends Model
{
    public function comments()
    {
        return $this->morphMany(Comments::class, 'commentable');
    }
}

Om nu het commentaar van een Live op te halen, kunnen we gewoon de find methode callen met het id, en nu hebben we toegang tot de commentaar iterable klasse:

<?php

use AppModelsLive;

$live = Live::find(1);

foreach ($live->comments as $comment) { }

// OR

Live::find(1)->comments()->each(function($comment) { // });
Live::find(1)->comments()->map(function($comment) { // });
Live::find(1)->comments()->filter(function($comment) { // });

// etc.

En als we het commentaar hebben en willen weten aan wie het toebehoort, dan hebben we toegang tot de commentable methode:

<?php

use AppModelsComment;

$comment = Comment::find(10);
$commentable = $comment->commentable;

// commentable – type of Post, Video, Poll, Live

Polymorphic one-of-many

In veel applicaties die schaalbaar zijn, willen we een gemakkelijke manier om te communiceren met modellen en tussen modellen. We willen misschien het eerste of laatste bericht van een gebruiker, wat kan worden gedaan met een combinatie van de methoden morphOne en ofMany:

<?php

public function latestPost()
{
    return $this->morphOne(Post::class, 'postable')->latestOfMany();
}

public function oldestPost()
{
    return $this->morphOne(Post::class, 'postable')->oldestOfMany();
}

De methoden latestOfMany en oldestOfMany halen het nieuwste of oudste model op op basis van de primaire sleutel van het model, wat de voorwaarde was dat het sorteerbaar is.

In sommige gevallen willen we niet sorteren op de ID, misschien hebben we de publicatiedatum van sommige berichten veranderd en willen we ze in die volgorde, niet op hun id.

Dit kan worden gedaan door 2 parameters door te geven aan de ofMany methode om hierbij te helpen. De eerste parameter is de key waarop we willen filteren en de tweede is de sorting methode:

<?php

public function latestPublishedPost()
{
    return $this->morphOne(Post::class, "postable")->ofMany("published_at", "max");
}

Met dit in gedachten is het mogelijk om hiervoor geavanceerdere relaties te construeren! Stel je voor dat we dit scenario hebben. We worden gevraagd om een lijst te genereren van alle huidige berichten in de volgorde waarin ze zijn gepubliceerd. Het probleem ontstaat wanneer we 2 berichten hebben met dezelfde published_at waarde en wanneer berichten gepland staan om in de toekomst geplaatst te worden.

Om dit te doen, kunnen we de volgorde waarin we de filters willen toepassen doorgeven aan de ofMany methode. Op deze manier ordenen we op published_at en als ze hetzelfde zijn, ordenen we op id. Ten tweede kunnen we een query functie toepassen op de ofMany methode om alle berichten uit te sluiten die gepland staan om gepubliceerd te worden!

<?php

public function currentPosts()
{
    return $this->hasOne(Post::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function ($query) {
        $query->where('published_at', '<', now());
    });
}

Polymorphic many-to-many

De polymorphic many-to-many is iets complexer dan de normale. Een veel voorkomende situatie is dat tags van toepassing zijn op meerdere assets binnen je applicatie. Op TikTok hebben we bijvoorbeeld tags die kunnen worden toegepast op video’s, shorts, verhalen, enz.

Met de polymoprhic many-to-many kunnen we een enkele tabel hebben met tags die zijn gekoppeld aan de video’s, shorts en verhalen.

De tabelstructuur is eenvoudig:

videos
    id – integer
    description – string

stories 
    id – integer
    description – string

taggables 
    tag_id – integer
    taggable_id – integer
    taggable_type – string

Als de tabellen klaar zijn, kunnen we het model maken en de morphToMany methode gebruiken. Deze methode accepteert de naam van de modelklasse en de ‘relatienaam’:

<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class Video extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

En hiermee kunnen we eenvoudig de inverse relatie definiëren. We weten dat we voor elk child model de methode morphedByMany willen callen:

<?php

namespace AppModels;
use IlluminateDatabaseEloquentModel;

class Tag extends Model
{
    public function stories()
    {
        return $this->morphedByMany(Story::class, 'taggable');
    }

    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    } 
}

En als we nu een Tag krijgen, kunnen we alle video’s en verhalen ophalen die met die Tag te maken hebben!

<?php
use AppModelTag;

$tag = Tag::find(10);
$posts = $tag->stories;
$videos = $tag->stories;

Eloquent optimaliseren voor snelheid

Wanneer je met Laravels Eloquent ORM werkt, is het essentieel om te begrijpen hoe je databasequeries kunt optimaliseren en de hoeveelheid tijd en geheugen die nodig is om gegevens op te halen kunt minimaliseren. Eén manier om dit te doen is door caching te implementeren in je applicatie.

Laravel biedt een flexibel cachingsysteem dat verschillende backends ondersteunt, zoals Redis, Memcached en bestandsgebaseerde caching. Door Eloquent queryresultaten te cachen, kun je het aantal databasequery’s verminderen, waardoor je applicatie sneller en waardevoller wordt.

Bovendien kun je de query builder van Laravel gebruiken om extra complexe query’s te maken, waardoor de prestaties van je applicatie nog verder worden geoptimaliseerd.

Samenvatting

Eloquent relaties zijn een krachtige feature van Laravel waarmee developers eenvoudig kunnen werken met gerelateerde gegevens. Van one-to-one tot many-to-many relaties, Eloquent biedt een eenvoudige en intuïtieve syntaxis om deze relaties te definiëren en te query’en.

Als Laravel deverloper kan het beheersen van Eloquent relaties je ontwikkelworkflow enorm verbeteren en je code efficiënter en leesbaarder maken. Als je meer wilt weten over Laravel, dan heeft Kinsta verschillende bronnen beschikbaar, waaronder een tutorial over aan de slag gaan met Laravel en een artikel over de salarissen van Laravel developers.

Kinsta biedt Managed Hosting oplossingen die het deployen en beheren van Laravel applicaties een fluitje van een cent maken.

Coman Cosmin

Cosmin Coman is een technologisch schrijver en developer met meer dan 3 jaar ervaring. Naast het schrijven voor Kinsta heeft hij meegewerkt aan onderzoek bij kernfysische faciliteiten en universiteiten. Hij is technisch onderlegd en geïntegreerd in de gemeenschap en komt altijd met innovatieve oplossingen.