다대다(Many to Many)
다대다 관계는 일대일이나 일대다 보다 매우 복잡한 관계 모델로 테이블 A 의 모델은 테이블 B 의 여러 개의 모델과 관계되어질 수 있으며 테이블 B 의 모델도 테이블 A 의 여러 개의 모델과 관계될 수 있습니다.
가장 좋은 예는 Users 테이블과 Roles 테이블로 블로그 서비스를 만들고 블로그의 사용자는 여러 가지 권한이 있다고 가정해 봅시다. 사용자 A는 블로그의 게시글 편집, 게시글 삭제, 댓글 수정등 여러 가지 권한을 가질 수 있습니다.
반대로 게시글 삭제, 댓글 수정 권한은 사용자 A 만이 아닌 사용자 B, C 도 가질 수 있습니다.
Eloquent 에서 다대다 관계를 지정하려면 중간에 테이블(피봇 테이블; pivot table)이 하나 더 필요합니다.
users, roles 테이블이 있을 경우 피봇 테이블 명은 두 개의 테이블 명을 각각 단수형으로 바꾸고 알파벳 순서가 빠른 테이블의 이름(role)을 쓰고 _ 로 이어서 알파벳 순서가 느린 테이블(user)을 붙여서 만들며 최종 피봇 테이블명은 role_user 여야 합니다.
피봇 테이블은 user_id, role_id 두 개의 키가 참조키로 존재해야 합니다.
다대다 관계 정의
그러면 다대다 관계를 구현하기 위해 먼저 User 모델에 관계되는 테이블명과 동일한 메소드를 만들고 belongsToMany($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null) 메소드를 호출해 주면 됩니다.
첫 번째 파라미터만 필수이며 관련된 테이블 모델명을 전달해 주면 됩니다.
class User
{
/**
* The roles that belong to the user.
*/
public function roles()
{
return $this->belongsToMany(Role::class);
}
}
이제 권한 테이블인 roles 테이블 생성하기 위해 migration 을 만들어 보겠습니다.
$ php artisan make:migration create_roles_table --create=roles
생성된 마이그레이션 파일의 up() 메소드에 권한명을 의미하는 컬럼인 $table->string('name'); 를 추가해 줍니다.
public function up()
{
Schema::create('roles', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->string('name');
});
}
이제 artisan make:model 명령어로 Role 모델 클래스를 생성합니다.
$ php artisan make:model Role
생성된 Role 모델도 마찬가지로 users() 메소드를 만들고 belongsTo() 메소드를 호출합니다.
class Role extends Model
{
public function users()
{
return $this->belongsToMany(User::class);
}
}
피봇 테이블을 만들어 주기 위해 새로운 migration 을 생성하며 피봇 테이블의 이름은 위에서 설명한 규칙으로 만들어야 하는 것을 주의하십시요.
$ php artisan make:migration create_role_user_table --create=role_user
피봇 테이블은 참조하는 두 개의 테이블을 가리키는 참조키가 필요하므로 생성해 줍니다.
public function up()
{
Schema::create('role_user', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
// users 테이블에 대한 참조키
$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users');
// roles 테이블에 대한 참조키
$table->integer('role_id')->unsigned();
$table->foreign('role_id')->references('id')->on('roles');
// user_id 와 role_id 컬럼은 유일해야 함.
$table->unique(['user_id', 'role_id']);
});
}
role_user 테이블에 대한 모델은 만들어 줄 필요가 없지만 모델 팩토리로 테스트 데이타를 넣기 위해 모델을 만들어 준 후에 작성한 migration을 적용합니다.
$ php artisan make:model RoleUser
$ php artisan migrate
RoleUser 모델은 관례상 role_users 테이블을 참조하지만 Eloquent 는 다대다 관계에서 피봇 테이블 이름이 role_user 여야 하므로 모델 팩토리로 테스트를 생성하면 에러가 발생하므로 RoleUser 모델의 테이블 명 관례를 수정해 줍니다.
class RoleUser extends Model
{
protected $table = 'role_user';
}
테스트 데이타 생성
이제 다대다 관계를 테스트 해보기 위해 모델 팩토리로 테스트 데이타를 생성하겠습니다. database/factories/ModelFactory.php 을 열어서 다음 팩토리를 추가해 줍니다.
$factory->define(App\Role::class, function ($faker) {
$role = ['guest', 'reporter', 'developer', 'owner', 'master'];
$num = $faker->numberBetween(0, 4);
return [
'name' => $role[$num] . $num,
'created_at' => $faker->dateTimeBetween($startDate = '-2 years', $endDate = '-1 years'),
'updated_at' => $faker->dateTimeBetween($startDate = '-1 years', $endDate = 'now'),
];
});
$factory->define(App\RoleUser::class, function ($faker) {
$user_id_min = App\User::min('id');
$user_id_max = App\User::max('id');
$role_id_min = App\Role::min('id');
$role_id_max = App\Role::max('id');
return [
'user_id' => $faker->numberBetween($user_id_min, $user_id_max),
'role_id' => $faker->numberBetween($role_id_min, $role_id_max),
'created_at' => $faker->dateTimeBetween($startDate = '-2 years', $endDate = '-1 years'),
'updated_at' => $faker->dateTimeBetween($startDate = '-1 years', $endDate = 'now'),
];
});
- RoleUser 테이블은 Users 와 Roles 테이블을 참조해야 하므로 User, Roles 테이블의 id 의 min, max 값을 가져와서 유효한 범위내에서 랜덤하게 user_id, role_id 를 할당합니다.
이제 tinker 로 테스트 데이타를 생성하면 다대다 관계를 테스트 할 준비가 완료됩니다.
$ php artisan tinker
User 테이블에 100 개의 데이타를 넣고 Role 에는 10개를 넣은 후에 피봇 테이블에 100 개의 데이타를 생성합니다.
>>> factory('App\User', 100)->create()
>>> factory('App\Role', 10)->create()
>>> factory('App\RoleUser', 100)->create()
id 가 1 인 사용자가 갖고 있는 역할들을 조회해 봅시다.
>>> $roles = App\User::find(1)->roles
=> <Illuminate\Database\Eloquent\Collection #00000000350884e8000000001eb79a29> [
<App\Role #00000000350884f9000000001eb79a29> {
id: 5,
created_at: "2013-08-08 06:39:56",
updated_at: "2014-09-21 18:46:04",
name: "master4",
pivot: <Illuminate\Database\Eloquent\Relations\Pivot #000000003508851e000000001eb79a29> {
user_id: 1,
role_id: 5
}
},
<App\Role #0000000035088502000000001eb79a29> {
id: 10,
created_at: "2014-03-01 04:28:45",
updated_at: "2015-01-02 09:35:48",
name: "developer2",
pivot: <Illuminate\Database\Eloquent\Relations\Pivot #0000000035088519000000001eb79a29> {
user_id: 1,
role_id: 10
}
},
결과중에 pivot 항목을 보면 피봇 테이블의 참조키 컬럼값을 확인할 수 있으며 user id 가 1 인 모델을 조회하였으므로 user_id 컬럼이 1 로 설정되어 있는 것을 볼 수 있습니다.
이제 반대로 Role 모델을 사용하여 특별 역할을 갖는 사용자들을 조회해 보겠습니다.
이번에는 Orm 컨트롤러에 다음 메소드를 추가합니다.
public function getManyToMany($id)
{
$users = Role::findOrFail($id)->users()->orderBy('name')->get();
dd($users);
}
이제 브라우저로 http://homestead.app/orm/many-to-many/1 연결하여 role id가 1번인 역할을 갖는 사용자를 조회해 봅시다.
다대다 관계는 실제로 많이 사용되는 관계이므로 잘 알아둘 필요가 있으며 eloquent 는 피봇 테이블에 추가 컬럼이 있을 경우 자동으로 가져오거나 timestamp 를 자동으로 설정하는 등 더 많은 기능이 있으므로 관심있는 독자들은 라라벨 매뉴얼(http://laravel.com/docs/5.1/eloquent-relationships#many-to-many)을 참고하시기 바랍니다.