Cours Laravel 9 Les Données La Relation NN
Cours Laravel 9 Les Données La Relation NN
Cours Laravel 9 Les Données La Relation NN
laravel.sillo.org/cours-laravel-9-les-donnees-la-relation-nn/
16 février 2022
Dans le précédent chapitre nous avons vu la relation de type 1:n, la plus simple et la plus
répandue. Nous allons maintenant étudier la relation de type n:n, plus délicate à
comprendre et à mettre en œuvre. Nous allons voir qu’Eloquent permet de simplifier la
gestion de ce type de relation.
Les données
La relation n:n
je peux avoir une ligne de la table A en relation avec plusieurs lignes de la table B,
je peux avoir une ligne de la table B en relation avec plusieurs lignes de la table A.
Cette relation ne se résout pas comme nous l’avons vu au chapitre précédent avec une
simple clé étrangère dans une des tables. En effet il nous faudrait des clés dans les deux
tables et plusieurs clés, ce qui n’est pas possible à réaliser.
La solution consiste à créer une table intermédiaire (nommée table pivot) qui sert à
mémoriser les clés étrangères. On va donc toujours avoir nos tables films et categories
mais en plus une table pivot entre les deux.
Les migrations
Au niveau des migration celle pour les catégories ne va pas changer. Pour celle des films
on va retirer la clé étrangère, donc il ne va rester que ça :
$table->id();
$table->string('title');
$table->year('year');
$table->text('description');
$table->timestamps();
$table->softDeletes();
});
1/11
Et il nous faut en plus la migration pour la table pivot qui elle aura 2 clés étrangères : une
pour les catégories et une autre pour les films :
Par convention on met les deux noms au singulier et dans l’ordre alphabétique donc
category_film.
$table->id();
$table->timestamps();
$table->foreignId('category_id')
->constrained()
->onUpdate('cascade')
->onDelete('cascade');
$table->foreignId('film_id')
->constrained()
->onUpdate('cascade')
->onDelete('cascade');
});
Cette fois j’ai choisi cascade pour les clés étrangères. Donc si on supprime une
catégorie ou un film la table pivot sera automatiquement mise à jour.
Et on rafraichit la base :
2/11
La relation entre les deux tables est assurée par la table pivot . Cette table pivot contient
les clés des deux tables :
De cette façon on peut avoir plusieurs enregistrements liés entre les deux tables, il suffit à
chaque fois d’enregistrer les deux clés dans la table pivot. Évidemment au niveau du
code ça demande un peu d’intendance parce qu’il y a une table supplémentaire à gérer.
Les modèles
return $this->belongsToMany(Film::class);
On déclare ici avec la méthode films (au pluriel) qu’une catégorie appartient à plusieurs
(belongsToMany) films (Film). On aura ainsi une méthode pratique pour récupérer les
films d’une catégorie.
return $this->belongsToMany(Category::class);
C’est le même principe que pour les catégories puisque la relation est symétrique. On
déclare avec la méthode categories (au pluriel) qu’un film appartient à plusieurs
(belongsToMany) catégories (Category).
La relation n:n
Voici une schématisation de cette relation avec les deux méthodes symétriques :
3/11
Toutes les méthodes qu’on a vues pour la relation 1:n fonctionnent avec la relation n:n,
ce qui est logique. Le fait qu’il y ait une table pivot ne change rien au fait que la relation,
vue de l’une des deux tables, ressemble à s’y méprendre à une relation 1:n. Si je choisis
une catégorie par exemple je sais qu’elle peut avoir plusieurs films liés.
La population
Il nous faut encore créer des enregistrements pour nos essais. On ne peut pas se
contenter de garder ce qu’on avait fait parce que maintenant ça serait bien qu’un film
appartienne à plusieurs catégories. Alors voilà le nouveau code de DatabaseSeeder :
Category::factory()->count(10)->create();
shuffle($ids);
});
4/11
Affichage d’un film et eager loading
Pour l’affichage d’un film on avait prévu de préciser la catégorie à laquelle appartenait ce
film. Il est évident qu’il va falloir prendre en compte maintenant le fait qu’on peut avoir
plusieurs catégories. On va mettre à jour la méthode show du contrôleur :
$film->with('categories')->get();
5/11
Collection {#265 ▼
#items: array:40 [▼
#fillable: array:3 [▼
0 => "title"
1 => "year"
2 => "description"
...
#attributes: array:7 [▼
"id" => 1
"description" => "Consequatur cumque odit delectus velit et. Sit non qui
harum vel quas autem numquam. Repellat ea praesentium voluptas fugit hic.
Voluptate ut tempore neque veni ▶"
...
#relations: array:1 [▼
#items: array:2 [▼
...
On appelle cette manière de faire l’eager loading (par opposition au lazy loading). Ça
permet d’éviter de multiples accès à la base pour aller récupérer des valeurs.
Maintenant dans la vue show on n’a plus de problème pour afficher les catégories :
6/11
@extends('template')
@section('content')
<div class="card">
<header class="card-header">
</header>
<div class="card-content">
<div class="content">
<hr>
<p>Catégories :</p>
<ul>
@foreach($film->categories as $category)
@endforeach
</ul>
<hr>
<p>Description :</p>
</div>
</div>
</div>
@endsection
7/11
Il y a quand même une chose qui me dérange. On a deux accès à la base :
Ça serait quand même plus élégant de tout faire d’un coup ! On va changer la liaison
implicite en liaison explicite. Dans le provider RouteServiceProvider qui, comme son
nom l’indique, est consacré aux routes, on va ajouter ce code :
use App\Models\Film;
...
...
});
Route::controller(FilmController::class)->group(function () {
Route::delete('films/force/{id}', 'forceDestroy')-
>name('films.force.destroy');
Route::put('films/restore/{id}', 'restore')->name('films.restore');
...
});
8/11
<label class="label">Catégories</label>
@foreach($categories as $category)
<option value="{{ $category->id }}" {{ in_array($category-
>id, old('cats') ?: []) ? 'selected' : '' }}>{{ $category->name }}</option>
@endforeach
</select>
</div>
array:5 [▼
"_token" =>
"qqARcLpGc6YDJ4jRpzazHeDbPlrxMSnSZYbtO9hJ"
0 => "1"
1 => "3"
$film = Film::create($filmRequest->all());
$film->categories()->attach($filmRequest->cats);
9/11
Modification d’un film
Ce qu’on a fait pour la création va nous servir pour la modification. D’ailleurs
précédemment on n’avait pas prévu la modification de la catégorie pour un film pour ne
pas trop alourdir le code, mais là on va le faire.
$view->with('categories', Category::all());
});
Ensuite dans la vue edit on ajoute la liste des catégories comme on l’a fait pour la vue
create :
<div class="field">
<label class="label">Catégories</label>
@foreach($categories as $category)
@endforeach
</select>
</div>
</div>
10/11
Le remplissage est un peu plus délicat parce qu’on a deux situations :
Laravel possède un système de collections très performant. Par exemple ici quand on
écrit $film->categories on obtient la collection de toutes les catégories du film. La
méthode pluck permet de ne garder que la clé qui nous intéresse, ici l’id. Enfin la
méthode all transforme la collection en tableau parce que c’est ce dont nous avons
besoin ici.
$film->update($filmRequest->all());
$film->categories()->sync($filmRequest->cats);
Pour modifier la table pivot on utilise cette fois la puissante méthode sync. On lui donne
un tableau en paramètre, si un enregistrement se trouve dans la table mais pas dans le
tableau alors il est supprimé et si une valeur se trouve dans la tableau mais pas dans la
table alors on crée l’enregistrement dans la table. C’est bien une synchronisation !
J’ai prévu un ZIP récupérable ici qui contient le code de cet article.
En résumé
Une relation de type n:n nécessite la création d’une table pivot.
L’eager loading permet de limiter le nombre d’accès à la base de données.
Si la liaison implicite ne suffit pas on peut faire de la liaison explicite avec un
traitement personnalisé.
Eloquent gère élégamment les tables pivots avec des méthodes adaptées (attach,
detach, sync…).
11/11