Many to many to many...
Mardi 4 octobre 2016 21:50
Je suis intervenu récemment sur une question dans le forum du site Laracasts. En résumé la question est : comment greffer une relation n:n sur une autre relation n:n. Autrement dit comment faire du n:n entre un pivot et une autre table en permettant de faire facilement des requêtes ?
La question m'a paru suffisamment intéressante pour rédiger cet article parce que ça permet de montrer comment utiliser intelligemment les relations et ne pas se limiter à un seul type de relation dans une situation particulière.
Les relations
La relation n:n de base
A la base on a une relation classique n:n entre une table users et une table roles avec un pivot : Jusque là pas de souci on a une situation standard qui respecte la normalisation des appellations. Un utilisateur peut avoir plusieurs rôles et un rôle peut être assumé par plusieurs utilisateurs. D'où la présence de la table pivot qui permet cette double multiplicité. Au niveau des relations dans le modèle User on a :public function roles() { return $this->belongsToMany('App\Role'); }Et le symétrique dans Role :
public function users() { return $this->belongsToMany('App\User'); }
La relation n:n ajoutée
Maintenant on veut ajouter une table tags en relation avec le pivot role_user. Autrement dit un utilisateur pour un certain rôle a plusieurs tags. Pour arriver à réaliser cela il faut :- prévoir un modèle pour la table pivot role_user,
- créer un nouveau pivot.
public function tags() { return $this->belongsToMany('App\Tag'); }Et dans le modèle Tag :
public function role_users() { return $this->belongsToMany('App\RoleUser'); }On peut donc créer des requêtes entre ces deux tables. Mais la difficulté qui arrive maintenant est de faire le lien entre les deux relations n:n ! Par exemple comment trouver pour les utilisateurs les rôles ainsi que les tags pour chaque rôle ? Avec les relations établies ci-dessus ce n'est pas possible, du moins pas simplement...
Ajouter des relations
Pour résoudre le problème il faut se rendre compte qu'une relation n:n est en fait une combinaison de deux 1:n. Si on considère par exemple la relation entre les utilisateurs et les rôles : A partir du moment où on a un modèle pour la table pivot on peut établir ces relations. En fait on transite par cette table pivot. L'avantage c'est qu'on peut ainsi biffurquer dans une autre direction ! On va donc ajouter la relation hasMany dans User pour se retrouver avec cette situation :public function roles() { return $this->belongsToMany('App\Role'); } public function role_users() { return $this->hasMany('App\RoleUser'); }De la même façon dans Role :
public function users() { return $this->belongsToMany('App\User'); } public function role_users() { return $this->hasMany('App\RoleUser'); }Et dans le pivot RoleUser :
public function user() { return $this->belongsTo('App\User'); } public function role() { return $this->belongsTo('App\Role'); } public function tags() { return $this->belongsToMany('App\Tag'); }On a maintenant toutes les relations qu'il nous faut !
Migrations et population
Migrations
Pour faire des essais il faut construire le schéma de la base, voici toutes les migrations : Pour la table users :<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; class CreateUsersTable extends Migration { public function up() { Schema::create('users', function(Blueprint $table) { $table->increments('id'); $table->timestamps(); $table->string('name'); }); } public function down() { Schema::drop('users'); } }Pour la table roles :
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; class CreateRoleUserTable extends Migration { public function up() { Schema::create('role_user', function(Blueprint $table) { $table->increments('id'); $table->timestamps(); $table->integer('user_id')->unsigned(); $table->integer('role_id')->unsigned(); }); } public function down() { Schema::drop('role_user'); } }Pour la table tags :
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; class CreateTagsTable extends Migration { public function up() { Schema::create('tags', function(Blueprint $table) { $table->increments('id'); $table->timestamps(); $table->string('name'); }); } public function down() { Schema::drop('tags'); } }Pour la table role_user :
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; class CreateRoleUserTable extends Migration { public function up() { Schema::create('role_user', function(Blueprint $table) { $table->increments('id'); $table->timestamps(); $table->integer('user_id')->unsigned(); $table->integer('role_id')->unsigned(); }); } public function down() { Schema::drop('role_user'); } }Pour la table role_user_tag :
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; class CreateRoleUserTagTable extends Migration { public function up() { Schema::create('role_user_tag', function(Blueprint $table) { $table->increments('id'); $table->timestamps(); $table->integer('role_user_id')->unsigned(); $table->integer('tag_id')->unsigned(); }); } public function down() { Schema::drop('role_user_tag'); } }Et enfin pour les contraintes :
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Eloquent\Model; class CreateForeignKeys extends Migration { public function up() { Schema::table('role_user', function(Blueprint $table) { $table->foreign('user_id')->references('id')->on('users') ->onDelete('restrict') ->onUpdate('restrict'); }); Schema::table('role_user', function(Blueprint $table) { $table->foreign('role_id')->references('id')->on('roles') ->onDelete('restrict') ->onUpdate('restrict'); }); Schema::table('role_user_tag', function(Blueprint $table) { $table->foreign('role_user_id')->references('id')->on('role_user') ->onDelete('restrict') ->onUpdate('restrict'); }); Schema::table('role_user_tag', function(Blueprint $table) { $table->foreign('tag_id')->references('id')->on('tags') ->onDelete('restrict') ->onUpdate('restrict'); }); } public function down() { Schema::table('role_user', function(Blueprint $table) { $table->dropForeign('role_user_user_id_foreign'); }); Schema::table('role_user', function(Blueprint $table) { $table->dropForeign('role_user_role_id_foreign'); }); Schema::table('role_user_tag', function(Blueprint $table) { $table->dropForeign('role_user_tag_role_user_id_foreign'); }); Schema::table('role_user_tag', function(Blueprint $table) { $table->dropForeign('role_user_tag_tag_id_foreign'); }); } }
Population
Enfin il nous faut des enregistrements :<?php use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { DB::table('users')->insert([ ['name' => 'user1'], ['name' => 'user2'], ['name' => 'user3'], ]); DB::table('roles')->insert([ ['name' => 'role1'], ['name' => 'role2'], ]); DB::table('tags')->insert([ ['name' => 'tag1'], ['name' => 'tag2'], ['name' => 'tag3'], ]); DB::table('role_user')->insert([ ['user_id' => 1, 'role_id' => 1], ['user_id' => 1, 'role_id' => 2], ['user_id' => 2, 'role_id' => 1], ['user_id' => 3, 'role_id' => 2], ]); DB::table('role_user_tag')->insert([ ['role_user_id' => 1, 'tag_id' => 1], ['role_user_id' => 1, 'tag_id' => 2], ['role_user_id' => 2, 'tag_id' => 2], ['role_user_id' => 2, 'tag_id' => 3], ['role_user_id' => 3, 'tag_id' => 1], ['role_user_id' => 3, 'tag_id' => 2], ['role_user_id' => 4, 'tag_id' => 1], ['role_user_id' => 4, 'tag_id' => 2], ['role_user_id' => 4, 'tag_id' => 3], ]); } }
Des requêtes
On a maintenant tout pour faire des requêtes ! Si on veut les utilisateurs avec leurs rôles on prend la relation classique :$users = \App\User::with('roles')->get(); foreach ($users as $user) { echo '<strong>' . $user->name . '<br></strong>'; foreach ($user->roles as $role) { echo '<li>' . $role->name . '</li>'; } }On obtient bien les rôles des utilisateurs : Maintenant si on veut la même chose mais pour chaque rôle les tags correspondants alors on transite par le modèle du pivot :
$users = \App\User::with('role_users.role', 'role_users.tags')->get(); foreach ($users as $user) { echo '<strong>' . $user->name . '<br></strong>'; foreach ($user->role_users as $role_user) { echo $role_user->role->name . ' :<br>'; foreach ($role_user->tags as $tag) { echo '<li>' . $tag->name . '</li>'; } } echo '<br>'; }On obtient bien ce qu'on voulait : On peut évidemment faire le même genre de requête en partant des rôles.
Par bestmomo
Nombre de commentaires : 18