如何使用 Model Factory 建立關聯性資料 ?
實務上我們會使用Seeder + Model Factory來建立測試的假資料,由於關聯性資料的primary key與foreign key必須成對出現,若使用Laravel的Relation,則可很方便的建立測試資料。
Version
PHP 7.0.0
Laravel 5.2.6
沒有使用Relation
如經典的users與posts table,一個user可以只有一篇post,所謂一對一關係;也可以一個user有多篇post,所謂一對多關係,其中primary key為users.id,foreign key為posts.user_id。
1 | use App\Post; |
第14行1
2
3
4
5
6$factory->define(Post::class, function (Generator $faker) {
return [
'title' => $faker->sentence,
'content' => $faker->paragraph,
];
});
使用model factory建立posts的假資料,雖然posts有user_id、title與content三個欄位,但因為user_id為foreign key,必須與users的id連動,所以無法使用faker建立。
1 | use App\User; |
使用seeder建立users的假資料,以前我們會在seeder使用faker,現在則在model factory使用faker,因此seeder只要告訴model factory要建立幾筆資料即可。
1 | use App\Post; |
因為users與posts為一對一關係,所以users建立10筆資料,posts也必須建立10筆資料。
傳統會使用for迴圈的方式產生$userId :1
2
3
4
5for($userId = 1; $userId <= 10; $userId++) {
factory(Post::class)->create([
'user_id' => $userId,
]);
}
若使用factory()->create()建立假資料時,若有某些欄位不想透過model factory產生,可以將陣列傳入create(),其中key為欄位名稱,而value為自訂假資料。
13行1
2
3
4
5collect(range(1, 10))->each(function (int $userId) {
factory(Post::class)->create([
'user_id' => $userId
]);
});
這裡改用closure的方式。
其中collect()為Laravel的helper function,相當於new Collection()。
each()為collection所提供,會iterate collection中每個item,效果相當於foreach,並可將collection中的item傳入closure,也就是range(1, 10)的會依序傳入$userId。
使用Relation
Laravel的eloquent提供了Relation機制,seeder與model factory可以使用更簡單的方式新增關聯性資料。
一對一資料
1 | namespace App; |
26行1
2
3
4
5
6
7
8
9/**
* 建立users與posts一對一關係
*
* @return HasOne
*/
public function post() : HasOne
{
return $this->hasOne(Post::class);
}
新增post(),描述users與posts的一對一關係,hasOne()的第一個參數傳入為一對一關係的class字串。1 1若foreign key的命名依照pararent tabel名稱 + id的命名方式,Laravel將可自動抓到foreign key與primary key的對應關係,不用特別指定。
Model factory的寫法不變。
1 | use App\Post; |
14行1
2
3
4
5factory(User::class, 10)
->create()
->each(function (User $user) {
$user->post()->save(factory(Post::class)->make());
});
有了relation之後,UserTableSeeder寫法有了重大的改變。
關鍵在於model factory的create()除了可新增資料外,還會傳回所新增資料的collection,因此可以使用fluent的寫法,繼續使用collection的each()來新增posts的一對一資料。
Closure會傳進每一筆User model,也就是$user,然後藉由$user->post()的relation找到一對一的Post model。
這裡用到了一個相當漂亮的技巧,使用了factory()->make()產生了Post model,注意這裡不是使用create(),而是使用make(),因為create()會直接存檔,但是Post的model factory並沒有描述id與user_id,所以只使用make()產生Post model,然後傳給save()去產生id與user_id存檔。2 2事實上這個漂亮的寫法並不是我寫出來的,而是出自於Laravel的官方說明文件Database: Seeding, Using Model Factory
有了relation之後,也不需要PostTableSeeder了,UserTableSeeder已經把users與posts的假資料都搞定了。
一對多資料
1 | namespace App; |
27行1
2
3
4
5
6
7
8
9/**
* 建立uses與posts一對多關係
*
* @return HasMany
*/
public function posts() : HasMany
{
return $this->hasMany(Post::class);
}
新增posts(),描述users與posts的一對多關係,hasMany()的第一個參數傳入為一對多關係的class字串。2 2若foreign key的命名依照pararent tabel名稱 + id的命名方式,Laravel將可自動抓到foreign key與primary key的對應關係,不用特別指定。
Model factory的寫法不變。
1 | use App\Post; |
14行1
2
3
4
5
6
7factory(User::class, 10)
->create()
->each(function (User $user) {
collect(range(1, 3))->each(function () use ($user) {
$user->posts()->save(factory(Post::class)->make());
});
});
之前一對一關係的寫法中,我們直接在closure內$user->post->save(),不過我們現在是一對多關係,假設我們現在每一筆user要對應3筆post。
這裡當然也是可以使用for迴圈的寫法,若使用collection->each()的風格,我們再引入一個新的closure,由於要使用到外部變數 : $user,所以必須使用user。3 3關於closure與use的用法,詳細請參考如何使用Closure?
Conclusion
- 使用relation,在seeder只要一行程式就可以完成一對多關係的假資料新增。
Collection->each()配合closure,其效果相當於for迴圈,但程式更加精簡。
Sample Code
完整的範例可以在我的GitHub上找到。