搭配Relation,只要一行就可建立關聯性資料

實務上我們會使用Seeder + Model Factory來建立測試的假資料,由於關聯性資料的primary key與foreign key必須成對出現,若使用Laravel的Relation,則可很方便的建立測試資料。

Version


PHP 7.0.0
Laravel 5.2.6

沒有使用Relation


如經典的usersposts table,一個user可以只有一篇post,所謂一對一關係;也可以一個user有多篇post,所謂一對多關係,其中primary key為users.id,foreign key為posts.user_id

若不使用relation,該如何對usersposts建立一對一關連性的假資料呢?
database/factories/ModelFactory.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use App\Post;
use App\User;
use Faker\Generator;

$factory->define(User::class, function (Generator $faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt(str_random(10)),
'remember_token' => str_random(10),
];
});

$factory->define(Post::class, function (Generator $faker) {
return [
'title' => $faker->sentence,
'content' => $faker->paragraph,
];
});

第14行

1
2
3
4
5
6
$factory->define(Post::class, function (Generator $faker) {
return [
'title' => $faker->sentence,
'content' => $faker->paragraph,
];
});

使用model factory建立posts的假資料,雖然postsuser_idtitlecontent三個欄位,但因為user_id為foreign key,必須與usersid連動,所以無法使用faker建立。

database/seeds/UserTableSeeder.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use App\User;
use Illuminate\Database\Seeder;

class UserTableSeeder extends Seeder
{

/**
* Run the database seeds.
*
* @return void
*/

public function run()
{

factory(User::class, 10)->create();
}
}

使用seeder建立users的假資料,以前我們會在seeder使用faker,現在則在model factory使用faker,因此seeder只要告訴model factory要建立幾筆資料即可。

database/seeds/PostTableSeeder.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use App\Post;
use Illuminate\Database\Seeder;

class PostTableSeeder extends Seeder
{

/**
* Run the database seeds.
*
* @return void
*/

public function run()
{

collect(range(1, 10))->each(function (int $userId) {
factory(Post::class)->create([
'user_id' => $userId
]);
});
}
}

因為usersposts一對一關係,所以users建立10筆資料,posts也必須建立10筆資料。

傳統會使用for迴圈的方式產生$userId :

1
2
3
4
5
for($userId = 1; $userId <= 10; $userId++) {
factory(Post::class)->create([
'user_id' => $userId,
]);
}

若使用factory()->create()建立假資料時,若有某些欄位不想透過model factory產生,可以將陣列傳入create(),其中key為欄位名稱,而value為自訂假資料

13行

1
2
3
4
5
collect(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

這種方式雖然可行,但必須另外在PostTableSeeder中靠for迴圈range(1, 10)產生user_id,是否有更快更直覺的方式呢?

使用Relation


Laravel的eloquent提供了Relation機制,seeder與model factory可以使用更簡單的方式新增關聯性資料。

一對一資料

app/User.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
namespace App;

use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{

/**
* The attributes that are mass assignable.
*
* @var array
*/

protected $fillable = [
'name', 'email', 'password',
];

/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/

protected $hidden = [
'password', 'remember_token',
];

/**
* 建立users與posts一對一關係
*
* @return HasOne
*/

public function post() : HasOne
{

return $this->hasOne(Post::class);
}
}

26行

1
2
3
4
5
6
7
8
9
/**
* 建立users與posts一對一關係
*
* @return HasOne
*/

public function post() : HasOne
{

return $this->hasOne(Post::class);
}

新增post(),描述usersposts一對一關係hasOne()的第一個參數傳入為一對一關係的class字串。1 1若foreign key的命名依照pararent tabel名稱 + id的命名方式,Laravel將可自動抓到foreign key與primary key的對應關係,不用特別指定。

Model factory的寫法不變。

database/seeds/UserTableSeeder.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use App\Post;
use App\User;
use Illuminate\Database\Seeder;

class UserTableSeeder extends Seeder
{

/**
* Run the database seeds.
*
* @return void
*/

public function run()
{

factory(User::class, 10)
->create()
->each(function (User $user) {
$user->post()->save(factory(Post::class)->make());
});
}
}

14行

1
2
3
4
5
factory(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並沒有描述iduser_id,所以只使用make()產生Post model,然後傳給save()去產生iduser_id存檔。2 2事實上這個漂亮的寫法並不是我寫出來的,而是出自於Laravel的官方說明文件Database: Seeding, Using Model Factory

有了relation之後,也不需要PostTableSeeder了,UserTableSeeder已經把usersposts的假資料都搞定了。

使用Relation之後,一對一關係的假資料果然可以很漂亮的新增,不過實務上更常見的是一對多關係的資料吧?

一對多資料

app/User.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
namespace App;

use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{

/**
* The attributes that are mass assignable.
*
* @var array
*/

protected $fillable = [
'name', 'email', 'password',
];

/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/

protected $hidden = [
'password', 'remember_token',
];


/**
* 建立uses與posts一對多關係
*
* @return HasMany
*/

public function posts() : HasMany
{

return $this->hasMany(Post::class);
}
}

27行

1
2
3
4
5
6
7
8
9
/**
* 建立uses與posts一對多關係
*
* @return HasMany
*/

public function posts() : HasMany
{

return $this->hasMany(Post::class);
}

新增posts(),描述usersposts一對多關係hasMany()的第一個參數傳入為一對多關係的class字串。2 2若foreign key的命名依照pararent tabel名稱 + id的命名方式,Laravel將可自動抓到foreign key與primary key的對應關係,不用特別指定。

Model factory的寫法不變。

database/seeds/UserTableSeeder.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use App\Post;
use App\User;
use Illuminate\Database\Seeder;

class UserTableSeeder extends Seeder
{

/**
* Run the database seeds.
*
* @return void
*/

public function run()
{

factory(User::class, 10)
->create()
->each(function (User $user) {
collect(range(1, 3))->each(function () use ($user) {
$user->posts()->save(factory(Post::class)->make());
});
});
}
}

14行

1
2
3
4
5
6
7
factory(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,所以必須使用user3 3關於closure與use的用法,詳細請參考如何使用Closure?

Conclusion


  • 使用relation,在seeder只要一行程式就可以完成一對多關係的假資料新增。
  • Collection->each()配合closure,其效果相當於for迴圈,但程式更加精簡。

Sample Code


完整的範例可以在我的GitHub上找到。

  1. 沒有使用Relation的一對一
  2. 使用Relation的一對一
  3. 使用Relation的一對多
2016-01-02