
Artikel bijgewerkt op 20 februari 2022 voor de nieuwe Laravel 9-versie
Laravel Eloquent is een van de sterkste onderdelen van het Laravel-framework, vooral dankzij de manier waarop je er relaties tussen tabellen mee definieert, aanmaakt en beheert. In deze tutorial laat ik je zien hoe je Eloquent-relaties opzet en gebruikt, zodat je aan de slag kunt zonder voorkennis van relaties. Lees daarnaast gerust de Laravel 9-tutorial voor beginners, die een andere invalshoek geeft om Laravel te leren met Eloquent.
Wat zijn relaties in een database?
Eerst even de absolute basis. Wat is een relatie precies? Een relatie betekent dat je twee of meer tabellen hebt met records die aan elkaar gekoppeld zijn. Stel dat je een tabel users hebt en dat elke gebruiker meerdere posts kan hebben. Hoe koppel je die twee? Meestal door een kolom user_id toe te voegen aan de tabel posts, zodat je meteen ziet welke gebruiker bij welk bericht hoort. Die ‘verbinding’, waarmee je bepaalt welke records bij elkaar horen, heet een relatie.
Welke soorten Eloquent-relaties bestaan er?
Er bestaan meerdere soorten Eloquent-relaties. Welk type je nodig hebt, hangt meestal af van de vraag of je aan de kant van de eerste en de tweede tabel één of (mogelijk) meerdere records hebt.
In het voorbeeld hierboven hebben we een tabel users en een tabel posts. Stel dat elk bericht maar bij één gebruiker kan horen. Dan voegen we een kolom user_id toe aan posts, want daar hoort de sleutel thuis: in de tabel posts. In het omgekeerde, onwaarschijnlijkere scenario, waarin elk bericht meerdere auteurs (users) kan hebben en elke auteur aan maar één bericht meewerkt, zetten we een kolom post_id op de tabel users. Dat is een vrij eenvoudig geval: een One-To-Many-relatie.
Maar wat als elke gebruiker meerdere berichten heeft én elk bericht meerdere auteurs? Hoe los je dat op? Dat heet een Many-To-Many-relatie. Bij dit complexere type kun je niet volstaan met één extra kolom in een tabel. Hier heb je een pivottabel nodig. Geen zorgen als dat nu ingewikkeld klinkt, verderop komen we erop terug.
Over het algemeen onderscheiden we de volgende soorten relaties tussen data:
- De One-To-One-relatie
- De One-To-Many-relatie
- Has-One-Of-Many (bijvoorbeeld het nieuwste record uit een reeks)
- De HasOneThrough- en HasManyThrough-relatie
- De Many-To-Many-relatie
One-To-One-relaties met Eloquent
We beginnen met de eenvoudigste relatie: het type ‘Has One’ of ‘One-To-One’. Dat betekent dat een bepaald record aan precies één ander record gekoppeld is, en dus niet aan meerdere.
We gaan verder met het voorbeeld van de blog, met gebruikers en berichten, en zeggen dat elke gebruiker één profiel kan hebben. In sommige gevallen zou je alle profielgegevens in het User-model kunnen stoppen, maar ideaal is dat niet. In dit voorbeeld wil ik ze in een aparte tabel bewaren. Willen we een profiel later aan een andere gebruiker overdragen, dan komt dat goed uit.
Maak eerst een model Profile aan, User wordt standaard al gegenereerd. Welke kolommen het model Profile precies krijgt, maakt hier weinig uit, maar je wilt waarschijnlijk het model en de bijbehorende migratie in één keer aanmaken:
php artisan make:model Profile -mOmdat het om een One-To-One-relatie gaat, mogen we bij uitzondering zelf kiezen:
- we zetten een
user_idop het modelProfile, - of we zetten een
profile_idop het modelUser.
Je mag kiezen wat je wilt, maar bedenk dat de relatie zegt dat een User een Profile heeft. Zodra iets iets anders heeft, komt die kolom meestal op het tweede model te staan. Je zou kunnen zeggen dat het eerste model het tweede ‘bezit’.
Dat is dus wat ik doe:
/** Voeg dit toe aan je migratie voor de profieltabel, of maak een nieuwe migratie */
$table->foreignId('user_id');Draai de migraties en we kunnen dit gaan implementeren.
Open eerst je model User en voeg de volgende publieke functie toe. Je mag de functie noemen wat je wilt, maar de conventie is snake_case:
public function profile()
{
return $this->hasOne(Profile::class);
}De methode hasOne verwacht nu dat profile() de kolom user_id van het model Profile gebruikt. Heet je kolom anders, geef hasOne dan een tweede argument met die kolomnaam mee:
return $this->hasOne(Profile::class, 'author_id');Toch is het niet aan te raden om van de conventie af te wijken.
Je hebt zojuist je eerste Eloquent-relatie toegevoegd. Scroll verder om te leren hoe je hem gebruikt.
De omgekeerde relatie toevoegen
In de meeste gevallen kun je ook de omgekeerde relatie definiëren. Dat klinkt ingewikkeld, maar dat valt mee. Het komt erop neer dat we op het model Profile een functie zetten die verwijst naar het model waartoe het profiel behoort, hier User:
public function user()
{
return $this->belongsTo(User::class, 'author_id');
}Een Eloquent-relatie gebruiken
Deze One-To-One-relatie in Eloquent gebruik je heel eenvoudig. Zodra je een instantie van User hebt, roep je $user->profile of $user->profile()->enchainerDautreMethodeIci() aan.
Dat kan omdat $user een instantie is van de klasse User en we op dat model de relatiemethode profile() hebben gezet.
Let op, en dit is belangrijk: je bereikt zowel ->profile als property als ->profile() als functie. Het verschil is dat ->profile gewoon een Eloquent-instantie teruggeeft, terwijl je aan ->profile() nog methodes kunt vastknopen, bijvoorbeeld ->profile()->orderBy('xxx', 'ASC')->get().
Kortom, ->profile is een snelkoppeling voor ->profile()->get().
$user = auth()->user();
$profile = $user->profile;
$name = $user->profile->display_name;
// Een profiel aanmaken
$profile = $user->profile()->create([
//
]);
$user = Profile::find(1)->user;De One-To-Many-relatie in Eloquent
Een andere heel belangrijke relatie, misschien wel de belangrijkste, is de één-op-veel-relatie. Ook bekend als de hasMany-relatie: ze legt vast dat één record veel andere records heeft. Ze lijkt sterk op de vorige.
In ons blogvoorbeeld zeggen we nu dat een profiel meerdere berichten heeft. Open je model Profile en voeg de volgende methode toe:
public function posts()
{
return $this->hasMany(Post::class);
//Of return $this->hasMany(Post::class, 'foreign_key');
}Dat betekent dat elk profiel veel berichten heeft. De omgekeerde kant bestaat ook:
public function profile()
{
return $this->belongsTo(Profile::class);
//Of return $this->belongsTo(Profile::class, 'foreign_key');
}Het gebruik lijkt sterk op wat we hierboven zagen, met dit verschil: de relatie geeft meerdere records terug, als een Eloquent-collectie.
Nu je de relatie als functie op het model hebt gezet, kun je hem weer als property aanspreken (->posts), wat hier een Eloquent-collectie teruggeeft in plaats van één model. En we kunnen hem ook als functie gebruiken (->posts()->where('created_at', '>', now()->subDays(14))->get() ).
$posts = Profile::find(1)->posts;
//Of $posts = Profile::find(1)->posts()->get();
foreach ($posts as post) {
// TODO
}
$lastPost = Profile::find(1)->posts()->latest()->first();Het nieuwste of het oudste record ophalen via een relatie
Soms wil je bij het definiëren van een hasMany-relatie alleen het nieuwste of alleen het oudste record ophalen. Laravel geeft je daarvoor twee handige methodes: ->latestOfMany() en ->oldestOfMany():
public function latestPost()
{
return $this->hasMany(Post::class)->latestOfMany();
}public function oldestPost()
{
return $this->hasMany(Post::class)->oldestOfMany();
}Beide methodes geven voortaan één record terug in plaats van een collectie.
$latestPost = Profile::find(1)->latestPost;Bevalt dat, maar heb je eigen filters of andere geavanceerde where()-clausules nodig? Bekijk dan dit voorbeeld uit de Laravel-documentatie:
/**
* Haal de actuele prijs van het product op
*/
public function currentPricing()
{
return $this->hasOne(Price::class)->ofMany([
'published_at' => 'max',
'id' => 'max',
], function ($query) {
$query->where('published_at', '<', now());
});
}HasOneThrough en HasManyThrough
Nu deze voorbeelden achter de rug zijn, gaan we een stap verder. We willen een relatie definiëren via een ander model.
Neem het voorbeeld hierboven, waarin elke User één Profile heeft en elk Profile veel Posts. Zo’n relatie via een ander model heb je nodig zodra je alle berichten van een gebruiker wilt ophalen. Je kunt niet rechtstreeks een hasMany(Post::class) op het model User zetten, want het model Post bevat geen user_id, alleen een profile_id. Een perfect geval voor een has-many-through-relatie.
Zo’n relatie definieer je vrijwel net zoals hierboven. Voeg in ons voorbeeld het volgende toe aan het model User:
public function posts()
{
return $this->hasManyThrough(Post::class, Profile::class);
}Het eerste argument is het model dat we willen bereiken, het tweede het tussenliggende model.
Een hasOneThough-relatie definiëren gaat vrijwel identiek aan een hasMany(), alleen moet je hier zorgen dat er aan het eind maar één record overblijft:
public function first_login()
{
return $this->hasOneThrough(FirstLogin::class, Profile::class);
}Many-To-Many-relaties
Nu de bovenstaande types behandeld zijn, gaan we naar geavanceerdere relaties. Bij een veel-op-veel-relatie kan elke User meerdere Profile-records hebben, bijvoorbeeld wanneer je met anderen samenwerkt, en kan elk Profile meerdere User-records hebben.
Hoe los je dat op? We kunnen niet zomaar een profile_id op de tabel met gebruikers zetten, want er zijn mogelijk meerdere profielen. En een user_id op het model Profile zetten kan evenmin, want er zijn mogelijk ook meerdere gebruikers.
Daarvoor heb je een soort tussentabel nodig. Die tussentabel heet een pivottabel en heeft in de meeste gevallen geen andere taak dan twee id-waarden in dezelfde rij te bewaren.
Bekijk het volgende scenario: bevat onze pivottabel een user_id en een profile_id, dan koppelen we een bepaalde User aan een bepaald Profile. En omdat dezelfde user_id zo vaak mag voorkomen als we willen, koppelen we één gebruiker aan zoveel profile_id-waarden als nodig. Andersom geldt hetzelfde: dezelfde profile_id mag terugkomen met telkens een andere user_id, zodat een profiel zoveel gebruikers krijgt als je wilt.
Hoe maak je een migratie voor een Laravel-pivottabel?
We beginnen met het aanmaken van de migratie die we nodig hebben. Voer het volgende commando uit. Let op hoe de tabelnaam wordt gevormd door de twee tabelnamen aan elkaar te plakken, allebei in het enkelvoud en in alfabetische volgorde. Je kunt daarvan afwijken, maar meestal heeft dat weinig zin.
php artisan make:migration create_profile_user_table --create=profile_userOpen nu het migratiebestand en voeg twee extra regels toe aan de methode up().
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProfileUserTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('profile_user', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->foreignId('profile_id');
$table->foreignId('user_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('profile_user');
}
}
Zoals je ziet is de structuur van een pivottabel heel eenvoudig. Draai nu de migraties.
Open daarna onze twee modellen en voeg de twee relatiefuncties toe. Op elk model is de methode een belongsToMany().
Zet dit in je model User:
/**
* Publieke functie om alle profielen op te halen waaronder deze gebruiker kan publiceren
*/
public function profiles()
{
return $this->belongsToMany(Profile::class);
}En dit in je model Profile:
/**
* Publieke functie om alle gebruikers op te halen die onder dit profiel kunnen publiceren
*/
public function users()
{
return $this->belongsToMany(User::class);
}Wijk je af van de standaard tabelnaam, geef je eigen tabelnaam dan mee als parameter aan de functie belongsToMany.
return $this->belongsToMany(Profile::class, 'user_role'); // De tabelnaam staat hier in omgekeerde alfabetische volgordeStandaard gaat Eloquent ervan uit dat de pivottabel de kolommen user_id en profile_id heeft. Noem je ze anders, geef die namen dan mee als derde en vierde parameter:
return $this->belongsToMany(Profile::class, 'user_role', 'nom_de_la_colonne_qui_represente_l_identifiant_de_ce_modele', 'nom_de_la_colonne_qui_represente_l_identifiant_de_la_cle_etrangere');
// Dit oogt onnodig ingewikkeld, dus hier een voorbeeld.
// Stel dat we dit op het User-model zetten, dan ziet het er standaard zo uit:
return $this->belongsToMany(Profile::class, 'user_role', 'user_id', 'profile_id');Een Many-To-Many-relatie in Eloquent gebruiken
Deze relatie gebruik je heel eenvoudig, en vergelijkbaar met wat we hierboven zagen. Bekijk deze korte voorbeelden:
$user = User::find(1);
$user->profiles->each(function ($item) use ($user) {
$profile->name = $user->firstname . ' ' . $user->lastname;
$profile->save();
});$profile = Profile::find(1);
$profile->users()->orderBy('created_at', 'desc');Data opslaan in de pivottabel
Het laatste deel van dit artikel gaat over data opslaan in de pivottabel. Ja, dat kan ook. Je hebt het niet in elk project nodig, maar er zijn zeker situaties waarin het van pas komt.
Even samenvatten: wat is een rij in een pivottabel? Wat stelt die voor? Een rij in een pivottabel is de relatie tussen twee records. Verwijderen we die ene rij, bestaat de relatie dan nog? Nee.
Daarom is dit belangrijk. Als elke rij in een pivottabel een relatie voorstelt, kun je ook informatie over die relatie in de pivottabel bewaren. Denk aan iets simpels als wanneer de relatie is aangemaakt of voor het laatst is bijgewerkt, via de kolommen created_at en updated_at.
Relatiegegevens ophalen uit de pivottabel
In het voorbeeld hierboven hebben we een migratie voor de pivottabel gemaakt. Die migratie bevatte al onze kolommen created_at en updated_at, via $table->timestamps();. Hoe kom je nu bij die gegevens?
Eerst moeten we het Eloquent vertellen: ons pivotmodel heeft nog een paar extra attributen die ik wil kunnen lezen, en dit zijn ze.
Dat doe je zo:
/**
* Publieke functie om alle profielen op te halen waaronder deze gebruiker kan publiceren
*/
public function profiles()
{
return $this->belongsToMany(Profile::class)->withPivot('created_at', 'updated_at', 'active');
}Of gebruik de handige helper ->withTimestamps(), die de kolommen created_at en updated_at meteen instelt:
/**
* Publieke functie om alle profielen op te halen waaronder deze gebruiker kan publiceren
*/
public function profiles()
{
return $this->belongsToMany(Profile::class)->withTimestamps()->withPivot('active');
}Nu kun je de pivottabel uitlezen op deze manier:
$user = User::find(1);
foreach ($user->profiles as $profile) {
echo $profile->pivot->created_at;
}Handig, toch?
Het pivotattribuut een andere naam geven
Er is nog een leuke truc die je kunt gebruiken, en dat is het attribuut pivot hernoemen. Eloquent hanteert een beschrijvende, expressieve stijl. De documentatie geeft het voorbeeld van een relatie tussen een podcast en een gebruiker. Die relatie is een abonnement, dus het zou vreemd staan om een abonnement met ‘pivot’ aan te duiden.
// Niet goed:
$user->podcasts()->first()->pivot->price;
// Goed:
$user->podcasts()->first()->subscription->price;Om het pivotattribuut te hernoemen gebruik je ->as($name) op je return $this->belongsToMany():
return $this->belongsToMany(Podcast::class)
->as('subscription')
->withTimestamps();Conclusie
Dat was een lang artikel. Fijn dat je tot hier hebt gelezen. Ik heb je alle verschillende soorten relaties tussen Eloquent-modellen laten zien. Ik hoop dat het je helpt bij het leren werken met Eloquent-relaties.
De relaties uit dit artikel zijn degene die je het vaakst zult gebruiken. Wat mij betreft kom je daar voorlopig ruimschoots mee weg.
Toch kom je situaties tegen waarin deze relaties tekortschieten. We hadden het steeds over de relatie tussen één model en één ander model. Maar denk aan het vrij gangbare geval waarin je zowel een Post als een Page wilt taggen. Dan volstaan de bovenstaande relaties niet, want we hebben de polymorfe relaties niet behandeld. Daarover heb ik al een inleiding op de polymorfe relaties van Eloquent geschreven, lees die vooral als je dieper op het onderwerp wilt ingaan


