Laravel Eloquent: Beziehungen zwischen Modellen definieren

Laravel Eloquent: Beziehungen zwischen Modellen definieren

Artikel am 20. Februar 2022 mit der neuen Laravel-Version 9 aktualisiert

Laravel Eloquent ist eine der Kernfunktionen des Laravel-Frameworks. Das liegt vor allem daran, wie gut es Beziehungen zwischen Datenbanktabellen definiert, anlegt und verwaltet. In diesem Tutorial zeige ich dir, wie du Eloquent-Beziehungen erstellst und einsetzt, ganz ohne Vorkenntnisse. Lies dazu auch das Laravel-9-Tutorial für Einsteiger, das einen anderen Weg zeigt, Laravel mit Eloquent zu lernen.

Was sind Beziehungen in Datenbanken?

Fangen wir ganz vorn an. Was genau ist eine Beziehung? Eine Beziehung bedeutet, dass du zwei oder mehr Tabellen hast, deren Datensätze miteinander verknüpft sind. Angenommen, du hast eine Tabelle users, und jeder Nutzer kann mehrere posts haben. Wie verbindest du beides? Üblicherweise, indem du der Tabelle posts eine Spalte user_id hinzufügst: So erkennst du zu jedem Beitrag sofort den zugehörigen Nutzer. Genau diese Verbindung, mit der du bestimmst, welche Datensätze zusammengehören, heißt Beziehung.

Welche Arten von Eloquent-Beziehungen gibt es?

Es gibt mehrere Typen von Eloquent-Beziehungen. Welcher davon passt, hängt in der Regel davon ab, ob auf der Seite der ersten und der zweiten Tabelle jeweils ein oder (möglicherweise) mehrere Einträge stehen.

Im Beispiel oben haben wir eine Tabelle users und eine Tabelle posts. Nehmen wir an, jeder Beitrag gehört zu genau einem Nutzer. Dafür ergänzen wir in der Tabelle posts eine Spalte user_id, damit hängt jeder Eintrag aus posts an genau einem Nutzer. Im umgekehrten, deutlich unwahrscheinlicheren Fall, in dem jeder Beitrag mehrere Autoren (users) hat und jeder Autor nur an einem Beitrag mitschreibt, käme eine Spalte post_id in die Tabelle users. Das ist ein noch recht einfacher Fall: eine Beziehung vom Typ One-To-Many.

Was aber, wenn jeder Nutzer mehrere Beiträge haben kann und jeder Beitrag mehrere Autoren? Wie löst man das? Das nennt sich Many-To-Many-Beziehung. Bei diesem komplexeren Typ reicht eine zusätzliche Spalte in einer der beiden Tabellen nicht mehr aus. Hier brauchen wir eine Pivot-Tabelle. Keine Sorge, falls das nach zu viel klingt: Wir kommen gleich darauf zurück.

Allgemein unterscheidet man die folgenden Beziehungstypen zwischen Daten:

  1. One-To-One-Beziehung, also „hat genau eins“
  2. One-To-Many-Beziehung, eins zu vielen
  3. Has-One-Of-Many-Beziehungen, etwa der neueste Datensatz von vielen
  4. HasOneThrough- und HasManyThrough-Beziehung
  5. Many-To-Many-Beziehung

One-To-One-Beziehungen mit Eloquent

Fangen wir mit der einfachsten Beziehung an: „Has One“ beziehungsweise „One-To-One“. Sie besagt, dass ein Datensatz mit genau einem anderen Datensatz verknüpft ist, nicht mit mehreren.

Bleiben wir beim Blog-Beispiel mit Nutzern und Beiträgen: Jeder Nutzer soll ein Profil haben. In manchen Fällen könntest du alle Profildaten im User-Modell ablegen, ideal wäre das aber nicht. In unserem Beispiel lege ich sie in einer eigenen Tabelle ab. Sobald wir ein Profil später einem anderen Nutzer zuordnen wollen, zahlt sich das aus.

Lege zuerst ein Modell Profile an, User wird standardmäßig schon mitgeliefert. Welche Spalten das Modell Profile genau hat, spielt hier kaum eine Rolle, aber du willst vermutlich Modell und Migration in einem Rutsch erzeugen:

shell
php artisan make:model Profile -m

Da es sich um eine One-To-One-Beziehung handelt, haben wir ausnahmsweise die Wahl:

  1. Wir setzen eine user_id auf das Modell Profile, oder
  2. wir setzen eine profile_id auf das Modell User.

Du kannst frei entscheiden, aber bedenke: Die Beziehung besagt, dass ein User ein Profile hat. Und wenn etwas etwas anderes hat, landet die Spalte üblicherweise im zweiten Modell. Man könnte sagen: Das erste Modell „besitzt“ das zweite.

Genau das mache ich hier:

php
/** Füge das in deine Migration der Tabelle profile ein oder lege eine neue Migration an */
$table->foreignId('user_id');

Führe die Migrationen aus, danach können wir mit der Umsetzung loslegen.

Öffne zuerst dein Modell User und ergänze die folgende public function. Du darfst die Methode nennen, wie du willst, üblich ist aber snake_case:

php
public function profile()
{
    return $this->hasOne(Profile::class);
}

Die Methode hasOne erwartet jetzt, dass profile() die Spalte user_id des Modells Profile verwendet. Heißt deine Spalte anders, gib hasOne den abweichenden Spaltennamen als zweites Argument mit:

php
return $this->hasOne(Profile::class, 'author_id');

Empfehlenswert ist es allerdings nicht, von der Konvention abzuweichen.

Damit steht deine erste Eloquent-Beziehung! Lies weiter, um zu sehen, wie du sie einsetzt👌.

Die Umkehrung dieser Beziehung ergänzen

In den meisten Fällen lässt sich auch die Umkehrung einer Beziehung definieren. Das klingt aufwendiger, als es ist: Wir ergänzen im Modell Profile eine Methode, die auf das Modell zurückverweist, zu dem es gehört, hier User:

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

Eine Eloquent-Beziehung verwenden

Diese One-To-One-Beziehung in Eloquent zu nutzen, ist denkbar einfach. Sobald du eine Instanz von User hast, kannst du $user->profile oder $user->profile()->enchainerDautreMethodeIci() aufrufen.

Das funktioniert, weil $user eine Instanz der Klasse User ist und wir diesem Modell die Beziehungsmethode profile() hinzugefügt haben.

Wichtig ist dabei: Du kannst sowohl ->profile als Eigenschaft als auch ->profile() als Methode ansprechen. Der Unterschied liegt darin, dass ->profile direkt eine Eloquent-Instanz zurückgibt, während ->profile() dir erlaubt, weitere Methoden anzuhängen, etwa ->profile()->orderBy('xxx', 'ASC')->get().

Kurz gesagt: ->profile ist die Abkürzung für ->profile()->get().

php
$user = auth()->user();

$profile = $user->profile;
$name = $user->profile->display_name;

// Ein Profil anlegen
$profile = $user->profile()->create([
   //
]);

$user = Profile::find(1)->user;

One-To-Many-Beziehungen in Eloquent

Die nächste sehr wichtige, vielleicht sogar die wichtigste Beziehung ist die One-To-Many-Beziehung. Sie ist auch als hasMany-Beziehung bekannt und beschreibt, dass ein Element viele andere Elemente hat. Sie ähnelt der vorherigen stark.

Zurück zum Blog-Beispiel: Ein Profil hat mehrere Beiträge. Öffne dein Modell Profile und ergänze die folgende Methode:

php
public function posts()
{
    return $this->hasMany(Post::class);
    //Oder return $this->hasMany(Post::class, 'foreign_key');
}

Damit hat jedes Profil viele Beiträge. Die Umkehrung gibt es ebenfalls:

php
public function profile()
{
    return $this->belongsTo(Profile::class);
    //Oder return $this->belongsTo(Profile::class, 'foreign_key');
}

Die Verwendung entspricht weitgehend dem, was wir oben gesehen haben, mit einem Unterschied: Diese Beziehung liefert mehrere Elemente zurück, nämlich eine Eloquent-Collection.

Da die Beziehung als Methode auf dem Modell definiert ist, kannst du sie wieder als Eigenschaft ansprechen (->posts), die hier eine Eloquent-Collection statt eines einzelnen Modells zurückgibt. Und genauso als Methode (->posts()->where('created_at', '>', now()->subDays(14))->get()).

php
$posts = Profile::find(1)->posts;
//Oder $posts = Profile::find(1)->posts()->get();

foreach ($posts as post) {
   // TODO
}

$lastPost = Profile::find(1)->posts()->latest()->first();

Den neuesten oder ältesten Datensatz über eine Beziehung holen

Manchmal willst du bei einer hasMany-Beziehung nur den neuesten oder nur den ältesten Datensatz holen. Dafür stellt Laravel zwei praktische Methoden bereit, ->latestOfMany() und ->oldestOfMany():

php
public function latestPost()
{
    return $this->hasMany(Post::class)->latestOfMany();
}
php
public function oldestPost()
{
    return $this->hasMany(Post::class)->oldestOfMany();
}

Beide Methoden geben jetzt ein einzelnes Modell zurück statt einer Collection.

php
$latestPost = Profile::find(1)->latestPost;

Klingt gut, du brauchst aber eigene Filter oder komplexere where()-Klauseln? Dann sieh dir dieses Beispiel aus der Laravel-Dokumentation an:

php
/**
 * Den aktuellen Preis des Produkts holen
 */
public function currentPricing()
{
    return $this->hasOne(Price::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function ($query) {
        $query->where('published_at', '<', now());
    });
}

HasOneThrough und HasManyThrough

Nach diesen Beispielen gehen wir einen Schritt weiter: Jetzt definieren wir eine Beziehung über ein weiteres Modell hinweg.

Nimm noch einmal das Beispiel von oben: Jeder User hat ein Profile, und jedes Profile hat viele Posts. Eine Beziehung über ein weiteres Modell brauchen wir zum Beispiel, um alle Beiträge eines Nutzers zu bekommen. Ein hasMany(Post::class) direkt auf dem Modell User geht nicht, denn das Modell Post kennt keine user_id, sondern nur eine profile_id. Das ist der Musterfall für eine Has-Many-Through-Beziehung.

So eine Beziehung definierst du sehr ähnlich wie oben. Ergänze in unserem Beispiel Folgendes im Modell User:

php
public function posts()
{
    return $this->hasManyThrough(Post::class, Profile::class);
}

Das erste Argument ist das Modell, auf das wir zugreifen wollen, das zweite das Zwischenmodell.

Eine hasOneThough-Beziehung zu definieren, funktioniert fast genauso wie hasMany(), du musst hier nur sicherstellen, dass am Ende genau ein Element steht:

php
public function first_login()
{
    return $this->hasOneThrough(FirstLogin::class, Profile::class);
}

Many-To-Many-Beziehungen

Nach den bisherigen Typen kommen wir zu den fortgeschritteneren Beziehungen. Bei einer Many-To-Many-Beziehung kann jeder User mehrere Profile haben, etwa wenn mehrere Personen zusammenarbeiten, und jedes Profile mehrere User.

Wie löst man das? Eine Spalte profile_id in der Nutzertabelle reicht nicht, denn es gibt potenziell mehrere Profile. Und eine Spalte user_id im Modell Profile reicht ebenso wenig, denn es gibt potenziell mehrere Nutzer.

Dafür brauchen wir eine Art Zwischentabelle. Diese Zwischentabelle heißt Pivot-Tabelle und hat in den meisten Fällen keine andere Aufgabe, als zwei id in derselben Zeile abzulegen.

Nimm folgendes Szenario: Hat unsere Pivot-Tabelle eine Spalte user_id und eine Spalte profile_id, können wir einen bestimmten User mit einem bestimmten Profile verknüpfen. Weil dieselbe user_id beliebig oft vorkommen darf, lässt sich ein Nutzer mit beliebig vielen Profilen verbinden: Jede weitere Zeile mit derselben user_id zeigt auf ein anderes Profil. Umgekehrt gilt dasselbe, dieselbe profile_id darf ebenso oft auftauchen, jedes Mal mit einer anderen user_id.

Wie erstellt man eine Migration für eine Pivot-Tabelle in Laravel?

Zuerst legen wir die nötige Migration an. Führe den folgenden Befehl aus. Beachte, wie der Tabellenname entsteht: durch Aneinanderhängen der beiden Tabellennamen, beide im Singular und in alphabetischer Reihenfolge. Du kannst davon abweichen, lohnt sich in der Regel aber nicht.

shell
php artisan make:migration create_profile_user_table --create=profile_user

Öffne jetzt die Migration und ergänze zwei weitere Zeilen in der Methode up().

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

Wie du siehst, ist der Aufbau einer Pivot-Tabelle denkbar einfach. Führe jetzt die Migrationen aus.

Öffne danach beide Modelle und ergänze die zwei Beziehungsmethoden. In beiden Modellen haben sie die Form belongsToMany().

Ergänze das im Modell User:

php
/**
 * Öffentliche Funktion, um alle Profile zu holen, unter denen dieser Nutzer veröffentlichen kann
 */
public function profiles()
{
    return $this->belongsToMany(Profile::class);
}

Und das im Modell Profile:

php
/**
 *  Öffentliche Funktion, um alle Nutzer zu holen, die unter diesem Profil veröffentlichen können
 */
public function users()
{
    return $this->belongsToMany(User::class);
}

Wenn du einen anderen Tabellennamen als den Standard wählst, gibst du deinen Tabellennamen als Parameter an die belongsToMany-Methoden weiter:

php
return $this->belongsToMany(Profile::class, 'user_role'); // Die Tabelle steht hier in umgekehrter alphabetischer Reihenfolge

Standardmäßig erwartet Eloquent in der Pivot-Tabelle die Spalten user_id und profile_id. Heißen sie bei dir anders, gibst du sie als dritten und vierten Parameter mit:

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

// Das wirkt unnötig kompliziert, deshalb hier ein Beispiel.
// Angenommen, wir ergänzen das im User-Modell; standardmäßig sähe das so aus:
return $this->belongsToMany(Profile::class, 'user_role', 'user_id', 'profile_id');

Eine Many-To-Many-Beziehung in Eloquent verwenden

Diese Beziehung zu nutzen, ist sehr einfach und läuft ähnlich wie oben. Sieh dir diese kurzen Beispiele an:

php
$user = User::find(1);

$user->profiles->each(function ($item) use ($user) {

$profile->name = $user->firstname . ' ' . $user->lastname;
   $profile->save();

});
php
$profile = Profile::find(1);

$profile->users()->orderBy('created_at', 'desc');

Daten in der Pivot-Tabelle speichern

Im letzten Teil dieses Artikels geht es darum, Daten in der Pivot-Tabelle zu speichern. Ja, auch das geht. Für jeden Anwendungsfall braucht man das nicht, aber es gibt durchaus Situationen, in denen es sich lohnt.

Kurz zusammengefasst: Was ist eine Zeile in einer Pivot-Tabelle? Wofür steht sie? Eine Zeile steht für die Beziehung zwischen zwei Datensätzen. Löschen wir eine bestimmte Zeile, existiert die Beziehung dann noch? Nein.

Genau deshalb ist das wichtig: Wenn jede Zeile einer Pivot-Tabelle für eine Beziehung steht, können wir auch Informationen über die Beziehung in der Pivot-Tabelle ablegen. Das können vergleichsweise banale Angaben sein, etwa wann die Beziehung entstanden ist oder zuletzt aktualisiert wurde, über die Spalten created_at und updated_at.

Beziehungsdaten aus der Pivot-Tabelle auslesen

Im Beispiel oben haben wir eine Migration für die Pivot-Tabelle angelegt. Die enthielt dank $table->timestamps(); bereits die Spalten created_at und updated_at. Wie kommst du also an diese Daten?

Zuerst müssen wir es Eloquent mitteilen: Unser Pivot-Modell hat noch weitere Attribute, auf die ich zugreifen will, und zwar diese.

Das geht so:

php
/**
 * Öffentliche Funktion, um alle Profile zu holen, unter denen dieser Nutzer veröffentlichen kann
 */
public function profiles()
{
    return $this->belongsToMany(Profile::class)->withPivot('created_at', 'updated_at', 'active');
}

Oder du nimmst die praktische Hilfsmethode ->withTimestamps(), die created_at und updated_at gleich mitnimmt:

php
/**
 * Öffentliche Funktion, um alle Profile zu holen, unter denen dieser Nutzer veröffentlichen kann
 */
public function profiles()
{
    return $this->belongsToMany(Profile::class)->withTimestamps()->withPivot('active');
}

Jetzt kannst du auf die Pivot-Tabelle zugreifen, und zwar so:

php
$user = User::find(1);

foreach ($user->profiles as $profile) {
   echo $profile->pivot->created_at;
}

Praktisch, oder?

Das Pivot-Attribut umbenennen

Es gibt noch einen weiteren netten Kniff, nämlich das Attribut pivot umzubenennen. Eloquent setzt auf eine beschreibende, sprechende Ausdrucksweise. Die Dokumentation nennt als Beispiel die Beziehung zwischen einem Podcast und einem Nutzer. Diese Beziehung ist ein Abonnement, und „pivot“ wäre dafür ein reichlich seltsamer Name.

php
// Nicht gut:
$user->podcasts()->first()->pivot->price;

// Gut:
$user->podcasts()->first()->subscription->price;

Um das Pivot-Attribut umzubenennen, hängst du ->as($name) an return $this->belongsToMany() an:

php
return $this->belongsToMany(Podcast::class)
                ->as('subscription')
                ->withTimestamps();

Fazit

Das war ein langer Artikel. Wenn du bis hierher gelesen hast: danke, dass du dabeigeblieben bist. Ich habe dir alle Beziehungstypen zwischen Eloquent-Modellen gezeigt und hoffe, dass dir das beim Einstieg in die Eloquent-Beziehungen weiterhilft.

Die hier gezeigten Beziehungen sind die, die du am häufigsten brauchst. Meiner Meinung nach reichen sie für den Anfang mehr als aus.

Trotzdem wirst du auf Situationen stoßen, in denen diese Beziehungen nicht ausreichen. Bisher ging es immer um die Beziehung zwischen einem Modell und einem anderen. Denk aber an den häufigen Fall, dass du zum Beispiel sowohl einen Post als auch eine Page taggen willst. Dafür reichen die obigen Beziehungen nicht mehr, denn polymorphe Beziehungen haben wir nicht behandelt. Ich habe dazu bereits eine Einführung in die polymorphen Beziehungen von Eloquent geschrieben, lies sie, wenn du tiefer einsteigen willst.

Laravel

Damien Flandrin Webentwickler seit 2010, Gründer von Gekkode und Email Impact. Jeder Artikel wird vor der Veröffentlichung an einem echten Projekt getestet. Kontakt
Newsletter

Neue Tests, Tutorials und Projekte, per E-Mail.

Reproduzierbare Tests, versionierter Code, datierte Ergebnisse. Niemals Spam.