填充数据库一般是为了测试,要想更好的模拟实际情况,一个常见的需求就是保证填充后的的数据符合模型的关联关系,也就是一对一或一对多关联关系中从属模型*_id 字段在主属模型 id 字段的范围内,或者多对多关联关系的中间表的 *_id 字段在两边模型的 id 字段范围内。

一、定义关联关系

在填充一对一或者一对多关联关系模型之前,必须先定义关联关系,因为在后面的填充过程中会用到其关联方法。以典型的模型关系为例:

  • 一对一:用户(App\User)-> 个人简介(App\Profile

    主属模型(用户模型)中定义关联方法:

    // app/User.php
    public function profile()
    {
      return $this->hasOne(Profile::class);
    }
    
  • 一对多:用户(App\User) -> 文章(App\Post

    主属模型(用户模型)中定义关联方法:

    // app/User.php
    public function posts()
    {
      return $this->hasMany(Post::class);
    }
    
  • 多对多:文章(App\Post)<-> 标签(App\Tag

    在两边的模型中都定义关联方法:

    // app/Post.php
    public function tags()
    {
      return $this->belongsToMany(Tag::class);
    }
    
    // app/Tag.php
    public function posts()
    {
      return $this->belongsToMany(Post::class);
    }
    

二、使用模型工厂

创建模型工厂(App\User 的模型工厂默认已创建):

$ php artisan make:factory ProfileFactory --model=Profile
$ php artisan make:factory PostFactory --model=Post
$ php artisan make:factory TagFactory --model=Tag

模型工厂的创建细节参考文档即可,略过不表。

三、使用填充器

创建填充器:

# 对于一对一和一对多关联关系,只需创建主属模型的填充器即可
$ php artisan make:seeder UserSeeder
# 对于多对多关联关系,既要创建关联模型各自的填充器,还要创建中间表的填充器。
# 由于在填充 User 模型时会自动填充 Post 模型,所以这里省略了 Post 模型的填充器。
$ php artisan make:seeder TagSeeder
$ php artisan make:seeder PostTagSeeder

3.1 一对一和一对多填充

只需运行主属模型的填充器,即可同时填充其从属模型。在本例中,只需运行在 App\User 模型的填充器,即可同时填充 App\ProfileApp\PostApp\user 模型的填充器 run 方法写法如下:

// database/seeds/UserSeeder.php
public function run()
{
    factory(App\User::class, 9)->create()->each(function ($user) {
        // 填充一对一的 Profile 模型
        $user->profile()->save(factory(App\Profile::class)->make());
        // 填充一对多的 Post 模型
        $user->posts()->saveMany(factory(App\Post::class, mt_rand(0,15))->make());
    });
}

factory() 辅助函数会返回一个工厂构建器(Illuminate\Database\Eloquent\FactoryBuilder)。当 factory() 方法有第二个整数参数时,工厂构建器的 create() 方法返回一个模型实例的集合并插入数据库。

这时就可以利用集合的 each() 方法遍历创建的 App\User 模型实例:
使用 App\User 模型的 save() 方法保存一对一关联关系中单个从属模型实例。
使用 App\User 模型的 saveMany() 方法保存一对多关联关系中从属模型实例的集合。

工厂构建器的 make() 方法与 create() 方法的区别在于 make() 只是创建实例,并没有插入数据库,所以 make() 方法返回的模型实例没有主键 idtimestamp 字段,也就不能在 make() 方法后使用 each() 方法链式填充。所以,如果 App\User 也有自己一对一的主属模型,比如 App\Country ,那么只能在 App\User 模型的工厂方法中使用随机数填充:

// database/factories/UserFactory.php
use Illuminate\Support\Facades\DB;

return [
  // ...
  'country_id' => mt_rand(DB::table('countries')->min('id'),DB::table('countries')->max('id')),
  // ...
];

3.2 多对多填充

多对多的填充是对两边模型填充后,再填充对中间表。上面的填充器已经负责了对 App\Post 模型的填充。只需要编写 App\Tag 模型的填充器,和中间表的填充器。

App\Tag 模型填充器非常简单:

// database/seeds/TagSeeder.php
public function run()
{
    factory(App\Tag::class,5)->create();
}

中间表的填充器唯一需要注意的是保证两个关联 Id 的唯一性。

// database/seeds/PostTagSeeder.php
use Illuminate\Support\Facades\DB;

public function run()
{
    $arr = [];
    $post_id_min = DB::table('posts')->min('id');
    $post_id_max = DB::table('posts')->max('id');
    $tag_id_min = DB::table('tags')->min('id');
    $tag_id_max = DB::table('tags')->max('id');

      // 组织数据
    foreach (range($post_id_min,$post_id_max) as $post_id) {
      foreach (array_random(range($tag_id_min,$tag_id_max),mt_rand(0,$tag_id_max)) as $tag_id) {
        $arr[] = [
          'post_id' => $post_id,
          'tag_id' => $tag_id,
        ];
      }
    }

    // 使用构建器填充
    DB::table('post_tag')->insert($arr);
}

3.3 填充

$ php artisan db:seed --class=UserSeeder
$ php artisan db:seed --class=TagSeeder
$ php artisan db:seed --class=PostTagSeeder
$ # 或者将各个填充器写入 DatabaseSeeder.php 一并填充
$ php artisan db:seed

其它操作:

$ # 回滚所有迁移后重新迁移,并使用 DatabaseSeeder 填充器填充数据库
$ php artisan migrate:refresh --seed
(完)