다형성 관계(Polymorphic Relations)
다형성 관계는 모델이 하나의 모델과 관련되지 않고 여러 개의 모델과 관련될 수 있는 관계로 관계형 DBMS 의 SQL 로는 표현할 수 없는 관계입니다.
예를 들어 아래와 같이 picture, user, product 세 개의 모델이 있을 경우 picture 모델은 user 모델에 속할 수도 product 모델에 속할 수도 있습니다.
즉 사진을 저장하고 있는 테이블은 사용자의 프로필 사진일 수도 있고 판매하는 제품의 사진일 수도 있습니다.
이때 User, Product 모델은 imageable 메소드를 사용하여 소유하고 있는 Picture 모델에 접근할 수 있습니다.
위 관계도에서 가장 중요한 필드는 필드는 pictures 테이블의 imageable_id 와 imageable_type 입니다.
imageable_id 는 user 또는 product 의 id 를 나타내며 imageable_type 은 picture 모델을 소유하고 있는 모델의 클래스 이름을 나타내며 imageable 관계를 엑세스 할 때 Eloquent ORM 이 어떤 타입의 소유 모델을 반환해야 하는지 알려줍니다.
그러면 위와 같은 다형성 관계를 직접 구현하기 위해 migration 을 생성해 봅시다.
$ php artisan make:migration create_product_table --create=products
$ php artisan make:migration create_picture_table --create=pictures
products migration 을 열어서 테이블에 price 컬럼을 추가해 주고 저장합니다.
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->integer('price');
$table->timestamps();
});
}
이제 picture migration 파일은 다형성 관계를 맺어주는 두 개의 컬럼(imageable_id, imageable_type)을 정의해 줍니다.
public function up()
{
Schema::create('pictures', function (Blueprint $table) {
$table->increments('id');
$table->string('title', 100);
$table->string('path', 200);
$table->integer('imageable_id');
$table->string('imageable_type', 50);
$table->timestamps();
});
}
migration 을 실행하고 모델 클래스를 생성합니다.
$ php artisan migrate
$ php artisan make:model Picture
$ php artisan make:model Product
다형성을 갖는 모델인 Picture는 다형성에 접근하는 메소드에 morphTo($name = null, $type = null, $id = null) 메소드를 사용하여 다형성을 선언해 줍니다.
class Picture extends Model
{
/**
* picture 의 모든 소유자 모델 리턴
*/
public function imageable()
{
return $this->morphTo();
}
}
이제 다형성 모델에 접근하는 모델은 morphMany($related, $name, $type = null, $id = null, $localKey = null) 를 사용하여 다형성 모델 클래스와 접근자를 지정할 수 있습니다.
class User extends Model
{
/**
* Get all of the User member's photos.
*/
public function pictures()
{
return $this->morphMany(Picture::class, 'imageable');
}
}
Product 모델에도 똑같이 pictures() 메소드에 morphMany() 를 사용하여 다형성 모델에 접근할 수 있게 만들어 줍니다.
class Product extends Model
{
/**
* Get all of the product's photos.
*/
public function pictures()
{
return $this->morphMany(Picture::class, 'imageable');
}
}
이제 모델 간의 관계 설정이 완료되었고 테스트를 위해 모델 팩토리 사용하여 테스트 데이타를 입력해 보겠습니다.
database/factories/ModelFactory.php 를 열어서 다음과 같이 Product 와 Picture모델 팩토리를 추가해 줍니다.
$factory->define(App\Product::class, function ($faker) {
return [
'name' => $faker->sentence,
'price' => $faker->randomNumber(5),
'created_at' => $faker->dateTimeBetween($startDate = '-2 years', $endDate = '-1 years'),
'updated_at' => $faker->dateTimeBetween($startDate = '-1 years', $endDate = 'now'),
];
});
이제 Picture 팩토리를 추가해 줍니다..
$factory->define(App\Picture::class, function ($faker) {
if (rand(0,1) == 0) :
$type = 'App\User';
$min = App\User::min('id');
$max = App\User::max('id');
else :
$type = 'App\Product';
$min = App\Product::min('id');
$max = App\Product::max('id');
endif;
return [
'title' => $faker->text,
'path' => $faker->image($dir = 'storage', $width = 640, $height = 480) ,
'imageable_id' => $faker->numberBetween($min, $max),
'imageable_type' => $type,
'created_at' => $faker->dateTimeBetween($startDate = '-2 years', $endDate = '-1 years'),
'updated_at' => $faker->dateTimeBetween($startDate = '-1 years', $endDate = 'now'),
];
});
if (rand(0, 1) == 0) : Picture 모델은 User 또는 Product 모델에 속할 수 있습니다. 0 또는 1 을 생성하여 0이면 User 모델에 속하게 하고 1면 Product 모델에 속하게 하기 위한 코드입니다.
'path' => $faker->image($dir = 'storage', $width=640, $height=480) : faker 에는 랜덤하게 이미지를 생성하는 기능이 있으므로 이를 사용하여 이미지를 생성하고 storage 폴더에 저장합니다.
'imageable_id' => $faker->numberBetween($min, $max) : User 혹은 Product 의 모델과 랜덤하게 연결합니다.
'imageable_type' => $type : 위 if 문 결과에 따라 'App\User' 또는 'App\Product' 가 설정됩니다.
이제 모델 팩토리 설정이 완료되었으니 tinker 를 띄운 후에 모델 팩토리를 통해 Product 와 Picture 를 생성합니다.
Picture 모델은 랜덤하게 이미지를 생성해 주는 http://lorempixel.com/ 에서 이미지를 받아오므로 시간이 꽤 걸립니다.
>>> factory('App\Product', 20)->create()
>>> factory('App\Picture', 50)->create()
이제 다형성 모델이 생성되었으므로 tinker 에서 user id 가 4인 모델이 갖고 있는 picture 들을 조회해 보겠습니다.
>>> $pics = App\User::find(4)->pictures
=> <Illuminate\Database\Eloquent\Collection #000000003c208c50000000002cd42b2b> [
<App\Picture #000000003c208c55000000002cd42b2b> {
id: 15,
path: "storage/758f52f09f3225e3f0490115af3da36f.jpg",
imageable_id: 4,
imageable_type: "App\\User",
created_at: "2013-09-26 11:13:36",
updated_at: "2015-04-23 23:32:26"
}
]
product id 가 4인 모델의 모든 이미지도 다음과 같이 간단하게 조회 가능합니다.
>>> $pics = App\Product::find(4)->pictures
이제 Picture 모델을 통해 해당 picture 의 소유자를 확인하는 작업은 브라우저에서 테스트 할 수 있도록 Orm 컨트롤러에 메소드를 추가합니다.
public function getMorphTo($id)
{
$pic = Picture::find($id);
$imageable = $pic->imageable;
dump($imageable);
}
이제 브라우저로 http://homestead.app/orm/morph-to/5 에 연결하여 $imageable 이 어떤 객체인지 확인해 보겠습니다. 독자들도 URI 의 id 를 바꿔 가면서 결과를 확인해 보면
$imageable 객체는 소유자 클래스에 따라 App\User 또는 App\Product 가 되는 것을 확인할 수 있습니다.