다대다 관계는 일대일이나 일대다 보다 매우 복잡한 관계 모델로 테이블 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);
    }
}
CODE


이제 권한 테이블인 roles 테이블 생성하기 위해 migration 을 만들어 보겠습니다.

$ php artisan make:migration create_roles_table --create=roles
CODE

 

생성된 마이그레이션 파일의 up() 메소드에 권한명을 의미하는 컬럼인 $table->string('name'); 를 추가해 줍니다.

public function up()
{
    Schema::create('roles', function (Blueprint $table) {
        $table->increments('id');
        $table->timestamps();
        $table->string('name');
    });
}
CODE

이제 artisan make:model 명령어로 Role 모델 클래스를 생성합니다.

$  php artisan make:model Role
CODE

 

생성된 Role 모델도 마찬가지로 users() 메소드를 만들고 belongsTo() 메소드를 호출합니다.

class Role extends Model
{
    public function users()
    {
        return $this->belongsToMany(User::class);
    }
} 
CODE

피봇 테이블을 만들어 주기 위해 새로운 migration 을 생성하며 피봇 테이블의 이름은 위에서 설명한 규칙으로 만들어야 하는 것을 주의하십시요.

$ php artisan make:migration create_role_user_table --create=role_user
CODE

 

피봇 테이블은 참조하는 두 개의 테이블을 가리키는 참조키가 필요하므로 생성해 줍니다.

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']);
    });
}
CODE

 

role_user 테이블에 대한 모델은 만들어 줄 필요가 없지만 모델 팩토리로 테스트 데이타를 넣기 위해 모델을 만들어 준 후에 작성한 migration을 적용합니다.

$ php artisan make:model RoleUser
$ php artisan migrate
CODE

RoleUser 모델은 관례상 role_users 테이블을 참조하지만 Eloquent 는 다대다 관계에서 피봇 테이블 이름이 role_user 여야 하므로 모델 팩토리로 테스트를 생성하면 에러가 발생하므로 RoleUser 모델의 테이블 명 관례를 수정해 줍니다.

class RoleUser extends Model
{
    protected $table = 'role_user';
}
CODE

 

테스트 데이타 생성

이제 다대다 관계를 테스트 해보기 위해 모델 팩토리로 테스트 데이타를 생성하겠습니다. 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'),
    ];
});
CODE
  • RoleUser 테이블은 Users 와 Roles 테이블을 참조해야 하므로 User, Roles 테이블의 id 의 min, max 값을 가져와서 유효한 범위내에서 랜덤하게 user_id, role_id 를 할당합니다.

 

이제 tinker 로 테스트 데이타를 생성하면 다대다 관계를 테스트 할 준비가 완료됩니다.

$ php artisan tinker
CODE

 

User 테이블에 100 개의 데이타를 넣고 Role 에는 10개를 넣은 후에 피봇 테이블에 100 개의 데이타를 생성합니다.

>>> factory('App\User', 100)->create()
>>> factory('App\Role', 10)->create()
>>> factory('App\RoleUser', 100)->create() 
CODE

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
           }
       },
CODE

결과중에 pivot 항목을 보면 피봇 테이블의 참조키 컬럼값을 확인할 수 있으며 user id 가 1 인 모델을 조회하였으므로 user_id 컬럼이 1 로 설정되어 있는 것을 볼 수 있습니다.

 

이제 반대로 Role 모델을 사용하여 특별 역할을 갖는 사용자들을 조회해 보겠습니다.

이번에는 Orm 컨트롤러에 다음 메소드를 추가합니다.

public function getManyToMany($id)
{
    $users = Role::findOrFail($id)->users()->orderBy('name')->get();
    dd($users);		
}
CODE

이제 브라우저로 http://homestead.app/orm/many-to-many/1 연결하여 role id가 1번인 역할을 갖는 사용자를 조회해 봅시다.

 

다대다 관계는 실제로 많이 사용되는 관계이므로 잘 알아둘 필요가 있으며 eloquent 는 피봇 테이블에 추가 컬럼이 있을 경우 자동으로 가져오거나 timestamp 를 자동으로 설정하는 등 더 많은 기능이 있으므로 관심있는 독자들은 라라벨 매뉴얼(http://laravel.com/docs/5.1/eloquent-relationships#many-to-many)을 참고하시기 바랍니다.