Laravel : Migrations & Seeding for production data

28,839

Solution 1

Laravel development is about freedom. So, if you need to seed your production database and think DatabaseSeeder is the best place to do so, why not?

Okay, seeder is mainly to be used with test data, but you'll see some folks using it as you are.

I see this important kind of seed as part of my migration, since this is something that cannot be out of my database tables and artisan migrate is ran everytime I deploy a new version of my application, so I just do

php artisan migrate:make seed_models_table

And create my seedind stuff in it:

public function up()
{
    $models = array(
        array('name' => '...'),
    );

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

Solution 2

I've often found myself wondering what the right answer to this is. Personally, I'd steer clear of using seeding to populate required rows in the database as you'll have to put a load of conditional logic in to ensure that you don't attempt to populate something that's already there. (Deleting and recreating the data is very inadvisable as you could end up with key mismatches and if you're using cascading deletes you may accidentally wipe a load of your database my mistake! ;-)

I put the 'seeding' of rows into the migration script as the chances are, the data will need to be there as part of the rollout process.

It's worth noting that you should use the DB class instead of Eloquent models to populate this data as your class structure could change over time which will then prevent you from re-creating the database from scratch (without rewriting history and changing you migration files, which I'm sure is a bad thing.)

I'd tend to go with something like this:

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();
}

This then allows me to 'seed' the town table easily when I first create it, and wont interfere with any additions made to it at run time.

Solution 3

This is what I use in production.

Since I run migration on each deployment

artisan migrate

I create a seeder (just to keep seeding data out of migration for easy access later) and then run that seeder along with the migration

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');
    }
}

I do not add this seed call to seeds/DatabaseSeeder.php to avoid running it twice on a new installation.

Solution 4

The Artisan Command Solution

  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

Other noteworthy things:

  • Upsert Functionality: If you ever want to alter any of the seeded rows, simply update them in your model and it was update your database values next time you deploy. It will never create duplicate rows.
  • Soft-Deletable Models: Note that you define deletions by setting deleted_at to true or false. The Artisan command will handle calling the correct method to delete or recover your record.

Problems With Other Mentioned Solutions:

  • Seeder: Running seeders in production is an abuse of the seeders. My concern would be that an engineer in the future would alter the seeders thinking that it's harmless since the documentation states that they are designed to seed test data.
  • Migrations: Seeding data in a migration is strange and a an abuse of the purpose of the migration. It also doesn't let you update these values once your migration has been run.
Share:
28,839
gontard
Author by

gontard

Developer @IntactileDesign

Updated on July 09, 2022

Comments

  • gontard
    gontard almost 2 years

    My application needs a pre registered data set to work. So i need to insert them in the database when i set up the application.

    Laravel propose two mechanisms :

    • Database migrations : "They allow a team to modify the database schema and stay up to date on the current schema state."
    • Database seeding : "Laravel also includes a simple way to seed your database with test data using seed classes."

    When I read this description, none of these solutions seems to be adapted.

    A similar question has been asked on stackoverflow and answered. The answer proposes to use the a database seeder to populate the database by detecting the current environment :

    <?php
    
    class DatabaseSeeder extends Seeder {
    
        public function run()
        {
                Eloquent::unguard();
    
                if (App::environment() === 'production')
                {
                    $this->call('ProductionSeeder');
                }
                else
                {
                    $this->call('StagingSeeder');
                }
        }
    
    }
    

    Of course, this solution works. But i am not sure that it is the right way to do this, because by inserting data using seeders you are losing all the advantages provided by the migration mechanism (database upgrate, rollback...)

    I want to know what is the best practice in this case.

  • gontard
    gontard about 10 years
    Thanks for your answer Antonio. I will seed using a migration.
  • gontard
    gontard almost 10 years
    Thanks you Dan for your contribution. I use a migration now and i think too it is the right place to seed data.
  • Thelonias
    Thelonias over 8 years
    I know this is over a year old, but does the above actually work? I would imagine you'd get a "not null violation" because you're not including the "created_at" and "updated_at" fields when inserting into the table.
  • Jason
    Jason over 8 years
    This is a great answer. The migrations manage the schema, but sometimes to do that you need to move data around. This all needs to be done in a strict sequence, and migrations are able to enforce this sequence.
  • iamcastelli
    iamcastelli about 8 years
    @Ryan I tried out something similar, and it does not fire violations. The timestamps are just assigned "NULL" values
  • Nanne
    Nanne about 6 years
    It's worth noting that you should use the DB class instead of Eloquent models this is a really good point, and is valid for not only your Eloquent models but for any app-specific code! Your migrations will run with the newest code on a new install, and so must be forward compatible.
  • Yevgeniy Afanasyev
    Yevgeniy Afanasyev almost 6 years
    I tried call seeding in migration and it was all right until it comes to production. Production freezes with this. Please see my question for details.
  • Amirreza Nasiri
    Amirreza Nasiri over 5 years
    ✏️ Note that migrate:make is not defined anymore. use make:migration instead.
  • jrebs
    jrebs about 4 years
    This is a good answer. One can debate exactly how to best execute the initialization of the data (IMO this solution is a bit fancier than necessary), but the reasons at the bottom are correct. Going by Laravel docs, both migrations and seeders are not really the appropriate place for initializing production data on a new install.
  • Adam Berry
    Adam Berry about 4 years
    @jrebs Another thing I didn't consider is that with this solution, you need to run an extra command in your setUp() method of your unit tests whereas they run the migrations by default to migrate your SQLLite database because each test. I still think migrations are a technically incorrect place to put seed data. But it does give the migration approach extra merit I suppose.
  • Joel Mellon
    Joel Mellon over 2 years
    10/10 This is the cleanest method of seeding production data. It allows coordinating (timing) schema creation and data insertion, eg. create table, seed it to create foreign keys for a dependent table, create dependent table.It also segregates seeders, and allows us to reuse them elsewhere, eg. in tests.