0 XP
?
Avancé60 min40 XP

Objectifs de cette leçon

  • Configurer les relations hasOne, hasMany, belongsTo
  • Travailler avec les relations many-to-many
  • Éager loading avec with()

Relations Eloquent 🔗

Pourquoi les relations ?

Une base de données relationnelle est composée de tables liées entre elles :

  • Un utilisateur a plusieurs articles
  • Un article appartient à un utilisateur
  • Un article a plusieurs commentaires
  • Un utilisateur peut avoir plusieurs rôles

Les relations Eloquent permettent de naviguer entre ces tables sans écrire de JOINs SQL.


1. hasOne — Relation 1:1 🧑‍🤝‍🧑

Un utilisateur a un profil. C'est une relation un-à-un.

// App/Models/User.php
class User extends Model
{
    public function profile()
    {
        return $this->hasOne(Profile::class);
        // 👇 SQL généré : SELECT * FROM profiles WHERE user_id = ?
    }
}
// Utilisation
$profile = $user->profile; // Retourne un seul objet Profile
echo $profile->bio; // "Développeur Laravel depuis 2025"

Structure de la table

users:         id | name | email
profiles:      id | user_id | bio | avatar | created_at

💡 La clé étrangère user_id est dans la table profiles. C'est la table enfant qui porte la clé.


2. hasMany — Relation 1:N 📝

Un utilisateur a plusieurs articles. C'est la relation la plus courante.

class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
        // 👇 SQL : SELECT * FROM posts WHERE user_id = ?
    }
}
// Utilisation
$posts = $user->posts; // Collection de Post
foreach ($posts as $post) {
    echo $post->title;
}

// Avec filtre
$published = $user->posts()->where('published', true)->get();

Structure de la table

users:  id | name | email
posts:  id | user_id | title | body | published | created_at

💡 $user->posts (sans parenthèses) retourne une Collection déjà chargée. $user->posts() (avec parenthèses) retourne un QueryBuilder pour chaîner des conditions.


3. belongsTo — Relation inverse N:1 🔙

Un article appartient à un utilisateur. C'est l'inverse de hasMany.

class Post extends Model
{
    public function author()
    {
        return $this->belongsTo(User::class, 'user_id');
        // 👇 SQL : SELECT * FROM users WHERE id = ?
    }
}
// Utilisation
$post = Post::find(1);
$author = $post->author; // Retourne un seul objet User
echo $author->name; // "Alice"

// Tu peux chaîner :
$post->author->name; // "Alice"

💡 Par convention, la clé étrangère est user_id (nom du modèle + _id). Tu peux la personnaliser avec le 2e argument.


4. belongsToMany — Relation N:N 🔗

Un utilisateur peut avoir plusieurs rôles, et un rôle peut être attribué à plusieurs utilisateurs.

Cette relation nécessite une table pivot (intermédiaire).

class User extends Model
{
    public function roles()
    {
        return $this->belongsToMany(Role::class);
        // 👇 SQL : SELECT roles.* FROM roles
        //         INNER JOIN role_user ON roles.id = role_user.role_id
        //         WHERE role_user.user_id = ?
    }
}

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

Table pivot : role_user

role_user: role_id | user_id | created_at

Utilisation

$roles = $user->roles; // Collection des rôles de l'utilisateur

// Vérifier si l'utilisateur a un rôle
if ($user->roles->contains('name', 'admin')) {
    // ...
}

// Ajouter un rôle
$user->roles()->attach(1); // Ajoute le rôle #1
$user->roles()->sync([1, 2, 3]); // Remplace tous les rôles
$user->roles()->detach(1); // Supprime le rôle #1

Données supplémentaires dans la pivot

class User extends Model
{
    public function roles()
    {
        return $this->belongsToMany(Role::class)
            ->withPivot('expires_at') // 👈 colonne supplémentaire
            ->withTimestamps();        // 👈 created_at, updated_at
    }
}

// Accéder aux données pivot
foreach ($user->roles as $role) {
    echo $role->pivot->expires_at;
}

5. Eager Loading — with() ⚡

Le problème N+1 🚨

// ❌ À ÉVITER
$posts = Post::all(); // 1 requête

foreach ($posts as $post) {
    echo $post->author->name; // N requêtes ! Une par article
}
// Total : 1 + 100 = 101 requêtes pour 100 articles

La solution : with() ✅

// ✅ Solution
$posts = Post::with('author')->get(); // 2 requêtes

foreach ($posts as $post) {
    echo $post->author->name; // Déjà en mémoire ! 0 requête
}
// Total : 2 requêtes seulement

Eager loading multiple

// Plusieurs relations
$users = User::with(['posts', 'profile', 'roles'])->get();

// Relations imbriquées
$users = User::with('posts.comments')->get();
// 3 requêtes : users + posts + comments

// Eager loading conditionnel
$users = User::with(['posts' => function ($query) {
    $query->where('published', true);
}])->get();

Comparaison des performances 📊

ApprocheRequêtes SQLTemps pour 100 articles
Lazy loading1 + 100 = 101~500ms
Eager loading2~10ms

💡 Règle d'or : Si tu sais que tu vas avoir besoin des relations, charge-les avec with() dès le départ.


6. Lazy loading vs Eager loading 🤔

CritèreLazy loadingEager loading (with())
Quand ?Au moment de l'accèsAu moment de la requête
Requêtes SQL1 + NNombre fixe
Performance lentrapide
Syntaxe$user->postsUser::with('posts')->get()
UsageDécouverte, petites donnéesListes, API, gros volumes

Bonnes pratiques ✅

  1. Toujours utiliser with() dans les contrôleurs qui retournent des listes
  2. Éviter N+1 — utilise with() dès que tu accèdes à une relation dans une boucle
  3. Nommer les relations clairement : author() plutôt que user() pour Post
  4. Utiliser has() pour filtrer : User::has('posts')->get()
  5. Utiliser withCount() pour les compteurs : User::withCount('posts')->get()
  6. Préférer les sous-requêtes aux boucles pour les agrégats

Pièges fréquents 🚨

PiègeSolution
❌ Oublier with() dans une boucleN+1 problem → l'API devient très lente
❌ hasMany sans $fillable dans l'enfantImpossible de créer via la relation
❌ belongsToMany sans table pivotErreur SQL : table manquante
❌ Confondre ->posts et ->posts()->posts = Collection, ->posts() = QueryBuilder
❌ Oublier foreignId dans la migrationImpossible de créer la clé étrangère

Exercices pour toi 🎯

  1. Modèle : Ajoute une relation hasMany de Category vers Post. Ajoute foreignId('category_id') dans la migration des posts.
  2. Requête : Écris une requête qui récupère tous les utilisateurs avec le nombre d'articles (withCount).
  3. Eager : Utilise with() pour charger les articles avec leur auteur et leurs commentaires.
  4. Pivot : Crée une relation belongsToMany entre Post et Tag avec une table pivot post_tag. Ajoute withTimestamps().
  5. Défi : Ouvre le simulateur ci-dessous. Parcours les 4 types de relations et observe le schéma, le code et le SQL pour chacune.

⚡ Simulateur — Relations Eloquent

Étape 1/6
📝 Pseudo-code
1# Relations Eloquent — lier les tables entre elles
2User → hasOne → Profile # ⬅ 1:1
3User → hasMany → Post # ⬅ 1:N
4Post → belongsTo → User # ⬅ N:1 (inverse)
5User → belongsToMany → Role # ⬅ N:N (pivot)
6User::with('posts')->get() # ⬅ eager loading
7// Lazy loading vs Eager loading (N+1 problem) # ⬅ optimisation
🔗 Relations Eloquent
1:1 hasOne1:N hasManyN:1 belongsToN:N belongsToMany with()⚠️ N+1
hasOne — Relation 1:1
Un utilisateur a UN profil.
📊 Schéma
users (id, name) ◀── profiles (id, user_id, bio, avatar)
🐘 Modèle
class User extends Model { public function profile() { return $this->hasOne(Profile::class); } }
class Profile extends Model { public function user() { return $this->belongsTo(User::class); } }
🗄️ SQL généré
SELECT * FROM profiles WHERE user_id = 1
💻 Utilisation
$profile = $user->profile; // Retourne le Profile de l'utilisateur #1
💡 Résumé des relations
hasOne
1:1 (user → profile)
hasMany
1:N (user → posts)
belongsTo
N:1 (post → user)
belongsToMany
N:N (user ↔ role)

hasOne() lie un modèle parent à un seul modèle enfant. La clé étrangère est dans la table enfant.