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_idest dans la tableprofiles. 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 📊
| Approche | Requêtes SQL | Temps pour 100 articles |
|---|---|---|
| Lazy loading | 1 + 100 = 101 | ~500ms |
| Eager loading | 2 | ~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ère | Lazy loading | Eager loading (with()) |
|---|---|---|
| Quand ? | Au moment de l'accès | Au moment de la requête |
| Requêtes SQL | 1 + N | Nombre fixe |
| Performance lent | rapide | |
| Syntaxe | $user->posts | User::with('posts')->get() |
| Usage | Découverte, petites données | Listes, API, gros volumes |
Bonnes pratiques ✅
- Toujours utiliser
with()dans les contrôleurs qui retournent des listes - Éviter N+1 — utilise
with()dès que tu accèdes à une relation dans une boucle - Nommer les relations clairement :
author()plutôt queuser()pour Post - Utiliser
has()pour filtrer :User::has('posts')->get() - Utiliser
withCount()pour les compteurs :User::withCount('posts')->get() - Préférer les sous-requêtes aux boucles pour les agrégats
Pièges fréquents 🚨
| Piège | Solution |
|---|---|
❌ Oublier with() dans une boucle | N+1 problem → l'API devient très lente |
❌ hasMany sans $fillable dans l'enfant | Impossible de créer via la relation |
| ❌ belongsToMany sans table pivot | Erreur SQL : table manquante |
❌ Confondre ->posts et ->posts() | ->posts = Collection, ->posts() = QueryBuilder |
❌ Oublier foreignId dans la migration | Impossible de créer la clé étrangère |
Exercices pour toi 🎯
- Modèle : Ajoute une relation
hasManydeCategoryversPost. AjouteforeignId('category_id')dans la migration des posts. - Requête : Écris une requête qui récupère tous les utilisateurs avec le nombre d'articles (
withCount). - Eager : Utilise
with()pour charger les articles avec leur auteur et leurs commentaires. - Pivot : Crée une relation
belongsToManyentrePostetTagavec une table pivotpost_tag. AjoutewithTimestamps(). - 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/6users (id, name) ◀── profiles (id, user_id, bio, avatar)class User extends Model { public function profile() { return $this->hasOne(Profile::class); } }class Profile extends Model { public function user() { return $this->belongsTo(User::class); } }SELECT * FROM profiles WHERE user_id = 1
$profile = $user->profile; // Retourne le Profile de l'utilisateur #1
1:1 (user → profile)
1:N (user → posts)
N:1 (post → user)
N:N (user ↔ role)
hasOne() lie un modèle parent à un seul modèle enfant. La clé étrangère est dans la table enfant.