如何使用 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上找到。