# EloquentORM关联关系之多对多
数据表之间往往不是孤立的,而是纵横交叉、相互关联的,比如一个用户对应多个角色,一个角色拥有多个用户等类似的多对多关联。
## 软件版本
* Laravel Version 5.4.19
* PHP Version 7.0.8
## 关键字和表
* `belongsToMany()`
* `attach()`
* `detach()`
* `sync()`
* `toggle()`
* `roles` 、`role_user` 和 `users` 表
* `User` 、`Role` 和 `RoleUser` 模型
一种常见的关联关系是多对多,即表A的某条记录通过中间表 C 与表 B 的多条记录关联,反之亦然。比如一个用户有多种角色,反之一个角色对应多个用户。
比如用户与角色组之间的关系,我们建立一个中间表 `role_user`,这个表关联用户表 `users` **(使用系统自带的users表)** 和 `roles` 表,如下
## 生成迁移文件和模型
```
php artisan make:migration create_role_user_table --create=role_user
php artisan make:model Role -m
```
### 编辑迁移文件
文件 `<project>/database/migrate/*_create_users_table.php` 内容如下
```
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email',30)->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
```
文件 `<project>/database/migrate/*_create_roles_table.php` 内容如下
```
Schema::create('roles', function (Blueprint $table) {
$table->increments('id')->comment('角色ID');
$table->string('name',20)->unique()->comment('角色英文名称');
$table->char('display_name',20)->nullable()->comment('角色中文名称');
$table->string('description',180)->nullable()->comment('角色简要描述');
$table->timestamps();
});
```
文件 `<project>/database/migrate/*_create_role_user_table.php` 内容如下
```
Schema::create('role_user' , function(Blueprint $table){
$table->unsignedInteger('user_id')->comment('用户id,关联users表');
$table->unsignedInteger('role_id')->comment('角色id,关联roles表');
$table->foreign('user_id')->references('id')->on('users')
->onUpdate('cascade')->onDelete('cascade');
$table->foreign('role_id')->references('id')->on('roles')
->onUpdate('cascade')->onDelete('cascade');
$table->primary(['user_id' , 'role_id']);
$table->timestamps();
});
```
### 运行 php artisan 命令保存修改到数据库
~~~
php artisan migrate
~~~
> 执行上面的命令后数据库将生成五张表,
> migrations
> password_resets
> users
> roles
> role_user
## 定义关联关系和修改模型的 fillable 属性
在 `User` 模型中定义与 `Role` 模型的对应关系:
```
public function roles()
{
/**
* @param string $related 关联关系
* @param string $table 关联中间表 不填这里默认为 role_user 规则为:Str::snake(class_basename($related)). '_' . Str::snake(class_basename($this)) 并在数据拼接前使用 sort() 排序;
* @param string $foreignKey 当前模型的外键id,不填默认为 user_id 规则为:Str::snake(class_basename($this)).'_'.$this->primaryKey;
* @param string $relatedKey 关联模型的外键id,不填默认为 role_id 规则为:Str::snake(class_basename($related)).'_'.$related->primaryKey
* @param string $relation 关联方法名 不填默认为roles
*/
return $this->belongsToMany('App\Role' , 'role_user' , 'user_id' , 'role_id' , 'roles')->withTimestamps();
}
```
在 `Role` 模型中定义与 `User` 模型的关联对应关系:
```
public function users()
{
/**
* @param string $related 关联关系
* @param string $table 关联中间表 不填默认为 role_user 规则为:Str::snake(class_basename($related)). '_' . Str::snake(class_basename($this)) 并在数据拼接前使用 sort() 排序;
* @param string $foreignKey 当前模型的外键id,不填默认为 role_id 规则为:Str::snake(class_basename($this)).'_'.$this->primaryKey;
* @param string $relatedKey 关联模型的外键id,不填默认为 user_id 规则为:Str::snake(class_basename($related)).'_'.$related->primaryKey
* @param string $relation 关联方法名 不填默认为 users
*/
return $this->belongsToMany(User::class , 'role_user' , 'role_id' , 'user_id' , 'users')
->withPivot(['created_at','updated_at']) // 中间表的字段,这里的中间表是 role_user
->withTimestamps();
}
```
> 如果想要中间表自动维护 `created_at` 和 `updated_at` 时间戳,可在定义关联方法时加上 `withTimestamps()` 方法
## 使用 tinker 填充数据
修改 `/databases/factories/ModelFactory.php`,新增关联数据。
~~~
/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(App\User::class , function(Faker\Generator $faker){
static $password;
return [
'name' => $faker->name ,
'email' => $faker->unique()->safeEmail ,
'password' => $password ? : $password = bcrypt('secret') ,
'remember_token' => str_random(10) ,
];
});
$factory->define(App\Role::class , function(Faker\Generator $faker){
return [
'name' => $faker->name ,
'display_name' => $faker->name ,
'description' => $faker->text(150) ,
];
});
$factory->define(App\RoleUser::class , function(Faker\Generator $faker){
$user_ids = \App\User::pluck('id')->toArray();
$role_ids = \App\User::pluck('id')->toArray();
return [
'user_id' => $faker->randomElement($user_ids) ,
'role_id' => $faker->randomElement($role_ids)
];
});
~~~
使用 tinker 命令
~~~
php artisan tinker
## 进入到 tinker 界面执行如下命令
namespace App
factory(User::class,4)->create(); // 生成4个用户
factory(Role::class,4)->create() // 生成4条 role_user 表的测试数据
~~~
## 关联操作
### 新增数据
#### 将用户关联到角色
```
$role_id = 2;
$user = \App\User::find(1);
$user->roles()->attach($role_id);
```
#### 将用户批量放入到角色
```
$role_ids = [1,3,4];
$user = \App\User::find(1);
$user->roles()->attach($role_ids);
// $user->roles()->attach([1 => ['attribute1' => 'value1'], 2, 3]);
```
有时可能想要使用一个命令,在建立新模型数据的同时附加关联。可以使用 `save`方法达成目的:
~~~
$role = new Role(['name' => 'Editor']);
\App\User::find(1)->roles()->save($role);
~~~
上面的例子里,新的 `Role` 模型对象会在储存的同时关联到 `user` 模型。也可以传入属性数组把数据加到关联数据库表:
~~~
\App\User::find(1)->roles()->save($role, ['field' => 'value']);
~~~
### 查询数据
查询用户所拥有的角色
```
$user = \App\User::find(1);
$roles = $user->roles;
dd($roles->toArray());
```
查询角色下属的所有用户
```
$role = \App\Role::find(2);
$users = $role->users;
```
### 关联删除
将用户从角色中移除
```
$role_id = 1;
$user = \App\User::find(1);
$user->roles()->detach($role_id);
```
将用户从所有角色中移除
```
$user = \App\User::find(1);
$user->roles()->detach();
```
删除角色下的所有用户关联数据
```
$role = \App\Role::find(2);
$role->users()->delete();
```
### 更新数据
#### 把用户"同步"到角色中
也可以使用 `sync` 方法附加关联模型。 `sync` 方法会把根据 ID 数组把关联存到中间表。附加完关联后,中间表里的模型只会关联到 ID 数组里的 id。
```
$user = \App\User::find(1);
$user->roles()->sync([1,2,4]);
$user->roles()->sync([1 => ['field' => 'value']]); // 加入其他字段的数据
```
#### 把角色"同步"给用户
```
$role = \App\Role::find(3);
$role->users()->sync([1]);
```
> 如果在定义 `belongsToMany()` 关联关系的时候,同时想操作中间关联表的数据,这里指的是`role_user` 表,那么可以定义 `with->withPivot($columns)` (参数填写中间表的字段)
那么,我们可以在使用 `attach()` 方法的时候传入第二个参数进行数据的同步更新,例如:
> ```
> dd($user->roles()->attach($role_id,['created_at'=>'2019-04-24 06:08:22']));
> ```
> 当然,如果单独需要更新中间表,这里指的是`role_user` 表的字段,可以使用 `updateExistingPivot()`,例如:
> ```
> $role_id = 2;
> $user = \App\User::find(1);
> $user->roles()->updateExistingPivot($role_id,['created_at'=>'2019-04-24 06:08:22']);
> ```
### 一些方法
#### `toggle`
顾名思义,如果表中存在则删除数据,如果表中不存在则新增数据。运用场景比如:点赞、喜欢或踩等切换操作。
```
$role_id = 2; // 入参 integer | array
$user = \App\User::find(1);
$user->roles()->toggle($role_id);
```
- 介绍
- Laravel5发送邮件使用Service隔离业务
- 如何使用Repository模式
- 如何使用Service模式
- 如何使用Presenter模式
- Laravel 5.* 执行迁移文件报错:Specified key was too long error
- EloquentORM关联关系
- EloquentORM关联关系之一对一
- EloquentORM关联关系之一对多
- EloquentORM关联关系之远层一对多
- EloquentORM关联关系之多对多
- EloquentORM关联关系之多态关联
- EloquentORM关联关系之多对多多态关联
- Laravel测试
- Laravel中涉及认证跳转地址的修改的地方
- Laravel中Collection的基本使用
- all
- avg
- chuck
- collapse
- combine
- contains
- containsStrict
- count
- diff
- diffAssoc
- diffKeys
- each
- every
- except
- filter
- first
- flatMap
- flatten
- flip
- forget
- forPage
- get
- groupBy
- has
- implode
- intersect
- intersectKey
- isEmpty
- isNotEmpty
- keyBy
- keys
- last
- map
- mapWithKeys
- max
- median
- merge
- min
- mode
- nth
- only
- partition
- pipe
- pluck
- pop
- prepend
- pull
- push
- put
- random
- reduce
- reject
- reverse
- search
- shift
- shuffle
- slice
- sort
- sortBy
- sortByDesc
- splice
- split
- sum
- take
- tap
- times
- toArray
- toJson
- transform
- union
- unique
- uniqueStrict
- values
- when
- where
- whereStrict
- whereIn
- whereInStrict
- whereNotIn
- whereNotInStrict
- zip
- Laravel中Collection的实际使用
- collection中sum求和
- collection格式化计算数据
- collection格式化计算数据计算github事件得分总和
- collection格式化markdown数据列表
- collection格式化计算两个数组的数据
- collection中reduce创建lookup数组
- TODO