Laravel5.2之Seeder填充數據小技巧

說明:本文主要聊一聊Laravel測試數據填充器Seeder的小技巧,同時介紹下Laravel開發插件三件套,這三個插件挺好用哦。同時,會將開發過程當中的一些截圖和代碼黏上去,提升閱讀效率。php

備註:在設計我的博客軟件時,總會碰到有分類Category、博客Post、給博客貼的標籤Tag、博客內容的評論Comment。
並且,Category與Post是一對多關係One-Many:一個分類下有不少Post,一個Post只能歸屬於一個Category;Post與Comment是一對多關係One-Many:一篇博客Post下有不少Comment,一條Comment只能歸屬於一篇Post;Post與Tag是多對多關係Many-Many:一篇Post有不少Tag,一個Tag下有不少Post。
開發環境:Laravel5.2 + MAMP + PHP7 + MySQL5.5laravel

開發插件三件套

在先聊測試數據填充器seeder以前,先裝上開發插件三件套,開發神器。先無論這能幹些啥,裝上再說。
一、barryvdh/laravel-debugbar數據庫

composer require barryvdh/laravel-debugbar --dev

二、barryvdh/laravel-ide-helperapp

composer require barryvdh/laravel-ide-helper --dev

三、mpociot/laravel-test-factory-helpercomposer

composer require mpociot/laravel-test-factory-helper --dev

而後在config/app.php文件中填上:dom

/**
         *Develop Plugin
        */
        Barryvdh\Debugbar\ServiceProvider::class,
        Mpociot\LaravelTestFactoryHelper\TestFactoryHelperServiceProvider::class,
        Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,

設計表的字段和關聯

設計字段

按照上文提到的Category、Post、Comment和Tag之間的關係建立遷移Migration和模型Model,在項目根目錄輸入:ide

php artisan make:model Category -m
php artisan make:model Post -m
php artisan make:model Comment -m
php artisan make:model Tag -m

在各個表的遷移migrations文件中根據表的功能設計字段:post

//Category表
class CreateCategoriesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name')->comment('分類名稱');
            $table->integer('hot')->comment('分類熱度');
            $table->string('image')->comment('分類圖片');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('categories');
    }
}
    
//Post表
class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('category_id')->unsigned()->comment('外鍵');
            $table->string('title')->comment('標題');
            $table->string('slug')->unique()->index()->comment('錨點');
            $table->string('summary')->comment('概要');
            $table->text('content')->comment('內容');
            $table->text('origin')->comment('文章來源');
            $table->integer('comment_count')->unsigned()->comment('評論次數');
            $table->integer('view_count')->unsigned()->comment('瀏覽次數');
            $table->integer('favorite_count')->unsigned()->comment('點贊次數');
            $table->boolean('published')->comment('文章是否發佈');
            $table->timestamps();
            //Post表中category_id字段做爲外鍵,與Category一對多關係
            $table->foreign('category_id')
                  ->references('id')
                  ->on('categories')
                  ->onUpdate('cascade')
                  ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        //刪除表時要刪除外鍵約束,參數爲外鍵名稱
        Schema::table('posts', function(Blueprint $tabel){
            $tabel->dropForeign('posts_category_id_foreign');
        });
        Schema::drop('posts');
    }
}

//Comment表
class CreateCommentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('post_id')->unsigned()->comment('外鍵');
            $table->integer('parent_id')->comment('父評論id');
            $table->string('parent_name')->comment('父評論標題');
            $table->string('username')->comment('評論者用戶名');
            $table->string('email')->comment('評論者郵箱');
            $table->string('blog')->comment('評論者博客地址');
            $table->text('content')->comment('評論內容');
            $table->timestamps();
            //Comment表中post_id字段做爲外鍵,與Post一對多關係
            $table->foreign('post_id')
                  ->references('id')
                  ->on('posts')
                  ->onUpdate('cascade')
                  ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
    //刪除表時要刪除外鍵約束,參數爲外鍵名稱
        Schema::table('comments', function(Blueprint $tabel){
            $tabel->dropForeign('comments_post_id_foreign');
        });
        Schema::drop('comments');
    }
}

//Tag表
class CreateTagsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name')->comment('標籤名稱');
            $table->integer('hot')->unsigned()->comment('標籤熱度');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('tags');
    }
}

因爲Post表與Tag表是多對多關係,還須要一張存放二者關係的表:測試

//多對多關係,中間表的命名laravel默認按照兩張表字母排序來的,寫成tag_post會找不到中間表
php artisan make:migration create_post_tag_table --create=post_tag

而後填上中間表的字段:ui

class CreatePostTagTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('post_tag', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('post_id')->unsigned();
            $table->integer('tag_id')->unsigned();
            $table->timestamps();
            //post_id字段做爲外鍵
            $table->foreign('post_id')
                  ->references('id')
                  ->on('posts')
                  ->onUpdate('cascade')
                  ->onDelete('cascade');
            //tag_id字段做爲外鍵      
            $table->foreign('tag_id')
                  ->references('id')
                  ->on('tags')
                  ->onUpdate('cascade')
                  ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('post_tag', function(Blueprint $tabel){
            $tabel->dropForeign('post_tag_post_id_foreign');
            $tabel->dropForeign('post_tag_tag_id_foreign');
        });
        Schema::drop('post_tag');
    }
}

設計關聯

寫上Migration後,還得在Model裏寫上關聯:

class Category extends Model
{
    //Category-Post:One-Many
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

class Post extends Model
{
    //Post-Category:Many-One
    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    //Post-Comment:One-Many
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    //Post-Tag:Many-Many
    public function tags()
    {
        return $this->belongsToMany(Tag::class)->withTimestamps();
    }
}

class Comment extends Model
{
    //Comment-Post:Many-One
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

class Tag extends Model
{
    //Tag-Post:Many-Many
    public function posts()
    {
        return $this->belongsToMany(Post::class)->withTimestamps();
    }
}

而後執行遷移:

php artisan migrate

數據庫中會生成新建表,表的關係以下:

Seeder填充測試數據

好,在聊到seeder測試數據填充以前,看下開發插件三件套能幹些啥,下文中命令可在項目根目錄輸入php artisan指令列表中查看。
一、barryvdh/laravel-ide-helper
執行php artisan ide-helper:generate指令前:
執行php artisan ide-helper:generate指令後:
不只Facade模式的Route由以前的反白了變爲能夠定位到源碼了,並且輸入Config Facade時還方法自動補全auto complete,這個很方便啊。

輸入指令php artisan ide-helper:models後,看看各個Model,如Post這個Model:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

/**
 * App\Post
 *
 * @property integer $id
 * @property integer $category_id 外鍵
 * @property string $title 標題
 * @property string $slug 錨點
 * @property string $summary 概要
 * @property string $content 內容
 * @property string $origin 文章來源
 * @property integer $comment_count 評論次數
 * @property integer $view_count 瀏覽次數
 * @property integer $favorite_count 點贊次數
 * @property boolean $published 文章是否發佈
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @property-read \App\Category $category
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Comment[] $comments
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Tag[] $tags
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereCategoryId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereTitle($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereSlug($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereSummary($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereContent($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereOrigin($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereCommentCount($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereViewCount($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereFavoriteCount($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post wherePublished($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereUpdatedAt($value)
 * @mixin \Eloquent
 */
class Post extends Model
{
    //Post-Category:Many-One
    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    //Post-Comment:One-Many
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    //Post-Tag:Many-Many
    public function tags()
    {
        return $this->belongsToMany(Tag::class)->withTimestamps();
    }
}

根據遷移到庫裏的表生成字段屬性和對應的方法提示,在控制器裏輸入方法時會自動補全auto complete字段屬性的方法:

二、mpociot/laravel-test-factory-helper
輸入指令php artisan test-factory-helper:generate後,database/factory/ModelFactory.php模型工廠文件會自動生成各個模型對應字段數據。Faker是一個好用的生成假數據的第三方庫,而這個開發插件會自動幫你生成這些屬性,不用本身寫了。

<?php

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| Here you may define all of your model factories. Model factories give
| you a convenient way to create models for testing and seeding your
| database. Just tell the factory how a default model should look.
|
*/

$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->safeEmail,
        'password' => bcrypt(str_random(10)),
        'remember_token' => str_random(10),
    ];
});
$factory->define(App\Category::class, function (Faker\Generator $faker) {
    return [
        'name' =>  $faker->name ,
        'hot' =>  $faker->randomNumber() ,
        'image' =>  $faker->word ,
    ];
});

$factory->define(App\Comment::class, function (Faker\Generator $faker) {
    return [
        'post_id' =>  function () {
             return factory(App\Post::class)->create()->id;
        } ,
        'parent_id' =>  $faker->randomNumber() ,
        'parent_name' =>  $faker->word ,
        'username' =>  $faker->userName ,
        'email' =>  $faker->safeEmail ,
        'blog' =>  $faker->word ,
        'content' =>  $faker->text ,
    ];
});

$factory->define(App\Post::class, function (Faker\Generator $faker) {
    return [
        'category_id' =>  function () {
             return factory(App\Category::class)->create()->id;
        } ,
        'title' =>  $faker->word ,
        'slug' =>  $faker->slug ,//修改成slug
        'summary' =>  $faker->word ,
        'content' =>  $faker->text ,
        'origin' =>  $faker->text ,
        'comment_count' =>  $faker->randomNumber() ,
        'view_count' =>  $faker->randomNumber() ,
        'favorite_count' =>  $faker->randomNumber() ,
        'published' =>  $faker->boolean ,
    ];
});

$factory->define(App\Tag::class, function (Faker\Generator $faker) {
    return [
        'name' =>  $faker->name ,
        'hot' =>  $faker->randomNumber() ,
    ];
});

在聊第三個debugbar插件前先聊下seeder小技巧,用debugbar來幫助查看。Laravel官方推薦使用模型工廠自動生成測試數據,推薦這麼寫的:

//先輸入指令生成database/seeds/CategoryTableSeeder.php文件: php artisan make:seeder CategoryTableSeeder
<?php

use Illuminate\Database\Seeder;

class CategoryTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(\App\Category::class, 5)->create()->each(function($category){
            $category->posts()->save(factory(\App\Post::class)->make());
        });
    }
}
//而後php artisan db:seed執行數據填充

可是這種方式效率並不高,由於每一次create()都是一次query,並且每生成一個Category也就對應生成一個Post,固然能夠在each()裏每一次Category繼續foreach()生成幾個Post,但每一次foreach也是一次query,效率更差。能夠用debugbar小能手看看。先在DatabaseSeeder.php文件中填上此次要填充的Seeder:

public function run()
    {
        // $this->call(UsersTableSeeder::class);
        $this->call(CategoryTableSeeder::class);
    }

在路由文件中寫上:

Route::get('/artisan', function () {
    $exitCode = Artisan::call('db:seed');
    return $exitCode;
});

輸入路由/artisan後用debugbar查看執行了15次query,耗時7.11ms:

實際上纔剛剛輸入幾個數據呢,Category插入了10個,Post插入了5個。
能夠用DB::table()->insert()批量插入,拷貝ModelFactory.php中表的字段定義放入每個表對應Seeder,固然能夠有些字段爲便利也適當修改對應假數據。

class CategoryTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
//        factory(\App\Category::class, 20)->create()->each(function($category){
//            $category->posts()->save(factory(\App\Post::class)->make());
//        });

        $faker = Faker\Factory::create();
        $datas = [];
        foreach (range(1, 10) as $key => $value) {
            $datas[] = [
                'name' =>  'category'.$faker->randomNumber() ,
                'hot' =>  $faker->randomNumber() ,
                'image' =>  $faker->url ,
                'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }
        DB::table('categories')->insert($datas);
    }
}

class PostTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $faker = Faker\Factory::create();
        $category_ids = \App\Category::lists('id')->toArray();
        $datas = [];
        foreach (range(1, 10) as $key => $value) {
            $datas[] = [
                'category_id' => $faker->randomElement($category_ids),
                'title' =>  $faker->word ,
                'slug' =>  $faker->slug ,
                'summary' =>  $faker->word ,
                'content' =>  $faker->text ,
                'origin' =>  $faker->text ,
                'comment_count' =>  $faker->randomNumber() ,
                'view_count' =>  $faker->randomNumber() ,
                'favorite_count' =>  $faker->randomNumber() ,
                'published' =>  $faker->boolean ,
                'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }
        DB::table('posts')->insert($datas);
    }
}

class CommentTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $faker = Faker\Factory::create();
        $post_ids = \App\Post::lists('id')->toArray();
        $datas = [];
        foreach (range(1, 50) as $key => $value) {
            $datas[] = [
                'post_id' => $faker->randomElement($post_ids),
                'parent_id' =>  $faker->randomNumber() ,
                'parent_name' =>  $faker->word ,
                'username' =>  $faker->userName ,
                'email' =>  $faker->safeEmail ,
                'blog' =>  $faker->word ,
                'content' =>  $faker->text ,
                'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }
        DB::table('comments')->insert($datas);
    }
}

class TagTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $faker = Faker\Factory::create();
        $datas = [];
        foreach (range(1, 10) as $key => $value) {
            $datas[] = [
                'name' =>  'tag'.$faker->randomNumber() ,
                'hot' =>  $faker->randomNumber() ,
                'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }
        DB::table('tags')->insert($datas);
    }
}

class PostTagTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $faker = Faker\Factory::create();
        $post_ids = \App\Post::lists('id')->toArray();
        $tag_ids = \App\Tag::lists('id')->toArray();
        $datas = [];
        foreach (range(1, 20) as $key => $value) {
            $datas[] = [
                'post_id' =>  $faker->randomElement($post_ids) ,
                'tag_id' =>  $faker->randomElement($tag_ids) ,
                'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }
        DB::table('post_tag')->insert($datas);
    }
}

在DatabaseSeeder.php中按照順序依次填上Seeder,順序不能顛倒,尤爲有關聯關係的表:

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // $this->call(UsersTableSeeder::class);
        $this->call(CategoryTableSeeder::class);
        $this->call(PostTableSeeder::class);
        $this->call(CommentTableSeeder::class);
        $this->call(TagTableSeeder::class);
        $this->call(PostTagTableSeeder::class);
    }
}

輸入路由/artisan後,生成了10個Category、10個Post、50個Comments、10個Tag和PostTag表中多對多關係,共有9個Query耗時13.52ms:

It is working!!!

表的遷移Migration和關聯Relationship都已設計好,測試數據也已經Seeder好了,就能夠根據Repository模式來設計一些數據庫邏輯了。準備趁着端午節研究下Repository模式的測試,PHPUnit結合Mockery包來TDD測試也是一種不錯的玩法。M(Model)-V(View)-C(Controller)模式去組織代碼,不少時候也未必指導性很強,給Model加一個Repository,給Controller加一個Service,給View加一個Presenter,或許代碼結構更清晰。具體可看下面分享的一篇文章。

最近一直在給本身充電,研究MySQL,PHPUnit,Laravel,上班並按時打卡,看博客文章,天天喝紅牛。不少不會,有些以前沒咋學過,哎,頭疼。後悔之前讀書太少,書到用時方恨少,人醜還需多讀書。

分享下最近發現的一張好圖和一篇極讚的文章:

文章連接:Laravel的中大型專案架構

相關文章
相關標籤/搜索