Laravel: 生产数据的迁移和填充

60

我的应用程序需要一个预先注册的数据集才能工作。因此,当我设置应用程序时,需要将它们插入数据库中。

Laravel 提供了两种机制:

  • 数据库迁移"它们允许团队修改数据库架构并保持最新的架构状态。"
  • 数据库填充"Laravel 还提供了一种使用种子类填充测试数据到数据库的简单方法。"

当我阅读这个描述时,似乎都不太适合我的情况。

stackoverflow 上问过类似的问题,并且有一个答案。该答案建议使用数据库填充器通过检测当前环境来填充数据库:

<?php

class DatabaseSeeder extends Seeder {

    public function run()
    {
            Eloquent::unguard();

            if (App::environment() === 'production')
            {
                $this->call('ProductionSeeder');
            }
            else
            {
                $this->call('StagingSeeder');
            }
    }

}
当然,这个解决方案是可行的。但我不确定这是否是正确的方法,因为通过使用seeder插入数据,您正在失去迁移机制提供的所有优势(数据库升级,回滚等)。
我想知道在这种情况下最佳实践是什么。

在Laravel中,迁移是关于模式管理而不是数据管理的。种子文件用于提供测试数据,但我认为它们的目的不是作为生产数据加载机制。 - warspite
5
是的,这正是文档上所说的。这就是我为什么要问这个问题的原因。 - gontard
2
也许这个软件包会有所帮助 https://github.com/slampenny/SmartSeeder - Karol F
@KarolFiturski 是的,它看起来很有前途。 - gontard
在 Laravel 5 中,我尝试将 seeder 和 migration 结合起来。一切都很顺利,直到进入生产环境。生产环境会因此而冻结。请查看我的问题以获取详细信息。 - Yevgeniy Afanasyev
5个回答

82
Laravel开发就是关于自由。所以,如果你需要在生产数据库中进行数据填充,并认为DatabaseSeeder是最好的地方,为什么不呢?
好吧,填充器主要用于测试数据,但你会看到一些人也在使用它。
我认为这种重要的填充是我迁移的一部分,因为这是不能离开我的数据库表的东西,每次我部署新版本的应用程序时,都会运行artisan migrate,所以我只需要这样做。
php artisan make:migration seed_models_table

在其中创建我的种子材料:
public function up()
{
    $models = array(
        array('name' => '...'),
    );

    DB::table('models')->insert($models);
}

1
感谢您的回答,Antonio。我将使用迁移来进行数据填充。 - gontard
4
这是一个很好的答案。迁移管理架构,但有时需要移动数据来完成。所有这些都需要按严格的顺序完成,而迁移可以强制执行此顺序。 - Jason
我尝试在迁移中使用数据填充(call seeding),一切都很好,直到进入生产环境。生产环境会因此冻结。详见我的问题 - Yevgeniy Afanasyev
5
请注意,migrate:make不再被定义。请改用make:migration - Amirreza Nasiri

43

我经常想知道这个问题的正确答案。就我个人而言,我会避免使用种子数据来填充数据库中所需的行,因为你必须放置大量的条件逻辑以确保你不会尝试填充已经存在的内容。(删除和重新创建数据是非常不可取的,因为这可能会导致键不匹配,并且如果你正在使用级联删除,你可能会错误地误删一堆你的数据库!;-)

我将行的“种植”放入迁移脚本中,因为很有可能在部署过程中需要这些数据。

值得注意的是,你应该使用 DB 类来填充这些数据,而不是 Eloquent 模型,因为你的类结构随时可能发生变化,这将防止你从头开始重新创建数据库(除非你重写历史并更改迁移文件,我相信这是一件坏事)。

我倾向于选择类似于以下的东西:

public function up()
{
    DB::beginTransaction();

    Schema::create(
        'town',
        function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->timestamps();
        }
    );

    DB::table('town')
        ->insert(
            array(
                array('London'),
                array('Paris'),
                array('New York')
            )
        );

    Schema::create(
        'location',
        function (Blueprint $table) {
            $table->increments('id');
            $table->integer('town_id')->unsigned()->index();
            $table->float('lat');
            $table->float('long');
            $table->timestamps();

            $table->foreign('town_id')->references('id')->on('town')->onDelete('cascade');
        }
    );
    
    DB::commit();
}

这使得我在第一次创建城镇表时能够轻松地“种子化”它,并且不会干扰任何在运行时添加到其中的内容。


感谢Dan的贡献。我现在使用迁移,我认为这也是种子数据的正确位置。 - gontard
2
我知道这已经过去一年了,但上面的代码真的有效吗?我想你会得到一个“非空违规”错误,因为在插入表时没有包括“created_at”和“updated_at”字段。 - Thelonias
1
@Ryan 我尝试了类似的东西,但它没有触发违规。时间戳只是被赋予了“NULL”值。 - iamcastelli
4
值得注意的是,您应该使用DB类而不是Eloquent模型。这是一个非常好的观点,不仅适用于您的Eloquent模型,也适用于任何特定于应用程序的代码!您的迁移将在新安装中运行最新的代码,因此必须具备向前兼容性。 - Nanne

23

这是我在生产中使用的。

由于我在每次部署时运行迁移。

artisan migrate

我创建了一个Seeder(仅为了将数据播种保持在迁移之外,以便以后轻松访问),然后与迁移一起运行该Seeder

class YourTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {    
        //migrate your table // Example
        Schema::create('test_table', function(Blueprint $table)
        {
            $table->increments('id');
            $table->timestamps();
            $table->softDeletes();
        });

        //seed this table
        $seeder = new YourTableSeeder();
        $seeder->run();
    }

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

我没有将这个种子调用添加到seeds/DatabaseSeeder.php中,以避免在新安装上运行两次。


这是种植生产数据最干净的方法。它允许协调(定时)模式创建和数据插入,例如创建表,播种以创建依赖表的外键,创建依赖表。它还分离了种子机,并允许我们在其他地方重复使用它们,例如在测试中。 - Joel Mellon
尽管我很想使用这个解决方案,但它总是导致我的测试失败或无限期运行。你遇到过这种情况吗? - Hashim Aziz

6

Artisan命令解决方案

  1. Create a new artisan command

    php artisan make:command UpsertConfigurationTables

  2. Paste this into the newly generated file: UpsertConfigurationTables.php

    <?php
    
    namespace App\Console\Commands;
    
    use Exception;
    use Illuminate\Console\Command;
    
    class UpsertConfigurationTables extends Command
    {
        /**
         * The name and signature of the console command.
         *
         * @var string
         */
        protected $signature = 'upsert:configuration';
    
        /**
         * The console command description.
         *
         * @var string
         */
         protected $description = 'Upserts the configuration tables.';
    
        /**
         * The models we want to upsert configuration data for
         *
         * @var array
         */
        private $_models = [
            'App\ExampleModel'
        ];
    
    
        /**
         * Create a new command instance.
         *
         * @return void
         */
        public function __construct()
        {
            parent::__construct();
        }
    
        /**
         * Execute the console command.
         *
         * @return mixed
         */
        public function handle()
        {
            foreach ($this->_models as $model) {
    
                // check that class exists
                if (!class_exists($model)) {
                    throw new Exception('Configuration seed failed. Model does not exist.');
                }
    
                // check that seed data exists
                if (!defined($model . '::CONFIGURATION_DATA')) {
                    throw new Exception('Configuration seed failed. Data does not exist.');
                }
    
                /**
                 * seed each record
                 */
                foreach ($model::CONFIGURATION_DATA as $row) {
                    $record = $this->_getRecord($model, $row['id']);
                    foreach ($row as $key => $value) {
                        $this->_upsertRecord($record, $row);
                    }
                }
            }
        }
    
        /**
         * _fetchRecord - fetches a record if it exists, otherwise instantiates a new model
         *
         * @param string  $model - the model
         * @param integer $id    - the model ID
         *
         * @return object - model instantiation
         */
        private function _getRecord ($model, $id)
        {
            if ($this->_isSoftDeletable($model)) {
                $record = $model::withTrashed()->find($id);
            } else {
                $record = $model::find($id);
            }
            return $record ? $record : new $model;
        }
    
        /**
         * _upsertRecord - upsert a database record
         *
         * @param object $record - the record
         * @param array  $row    - the row of update data
         *
         * @return object
         */
        private function _upsertRecord ($record, $row)
        {
            foreach ($row as $key => $value) {
                if ($key === 'deleted_at' && $this->_isSoftDeletable($record)) {
                    if ($record->trashed() && !$value) {
                        $record->restore();
                    } else if (!$record->trashed() && $value) {
                        $record->delete();
                    }
                } else {
                    $record->$key = $value;
                }
            }
            return $record->save();
        }
    
        /**
         * _isSoftDeletable - Determines if a model is soft-deletable
         *
         * @param string $model - the model in question
         *
         * @return boolean
         */
        private function _isSoftDeletable ($model)
        {
            $uses = array_merge(class_uses($model), class_uses(get_parent_class($model)));
            return in_array('Illuminate\Database\Eloquent\SoftDeletes', $uses);
        }
    }
    
  3. Populate $_models with the Eloquent models you want to seed.

  4. Define the seed rows in the model: const CONFIGURATION_DATA

    <?php
    
    namespace App;
    
    use Illuminate\Database\Eloquent\Model;
    use Illuminate\Database\Eloquent\SoftDeletes;
    
    class ExampleModel extends Model
    {
        use SoftDeletes;
    
        const CONFIG_VALUE_ONE = 1;
        const CONFIG_VALUE_TWO = 2;
        const CONFIGURATION_DATA = [
            [
                'id'         => self::CONFIG_VALUE_ONE,
                'col1'       => 'val1',
                'col2'       => 'val2',
                'deleted_at' => false
            ],
            [
                'id'         => self::CONFIG_VALUE_TWO,
                'col1'       => 'val1',
                'col2'       => 'val2',
                'deleted_at' => true
            ],
        ];
    }
    
  5. Add the command to your Laravel Forge deployment script (or any other CI deployment script): php artisan upsert:configuration

其他值得注意的事情:

  • Upsert功能: 如果你想要修改任何种子行,只需在模型中更新它们,下次部署时它将更新你的数据库值。它永远不会创建重复的行。
  • 可以软删除的模型: 注意,你通过将deleted_at设置为truefalse来定义删除。Artisan 命令将处理调用正确的方法来删除或恢复你的记录。

其他解决方案存在的问题:

  • Seeder: 在生产环境中运行 seeders 是对 seeders 的滥用。我担心的是未来的工程师会更改种子数据,因为文档说明它们是用于种植测试数据的,认为这是无害的。
  • Migrations: 在迁移中播种数据是奇怪的,并且滥用了迁移的目的。它也不允许在运行迁移后更新这些值。

1
这是一个好答案。人们可以辩论如何最好地执行数据初始化(在我看来,这个解决方案有点过于花哨),但底部的原因是正确的。根据Laravel文档,迁移和填充器都不是在新安装上初始化生产数据的真正适当的位置。 - jrebs
@jrebs 我没有考虑到的另一件事是,使用这种解决方案时,您需要在单元测试的setUp()方法中运行额外的命令,因为它们默认会运行迁移以迁移您的SQLLite数据库每个测试。我仍然认为迁移是放置种子数据的技术上不正确的位置,但我想这确实使迁移方法更有价值。 - Adam Berry

0
我也遇到了这个问题。最后我在我的迁移中添加了一个工匠命令来运行种子数据。
use Illuminate\Support\Facades\Artisan;

return new class extends Migration
{

    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->id();
    });

    Artisan::call('db:seed --class=DataSeeder --force');
}

这似乎是一个无意义的额外开销,当直接调用种子类(https://dev59.com/6GEi5IYBdhLWcg3wJZZe#54227917)是一个选择时。 - miken32

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接