dmytroshved's avatar

Seeders or Migrations for production data?

Hey everyone

Recently after some research I came up with a question: how to handle default / initial data population in a Laravel project: should it go into seeders or migrations? I’ve seen two very different schools of thought.

What other devs say

Nuno Maduro says:

Seeders should only be used for environment-specific data (local or test), and not for default system values. Database seeders become outdated over time and may no longer reflect the true state of your production data. This can introduce inconsistencies if you need to re-run migrations to deploy your app elsewhere. In contrast, migrations are inherently reliable because they define the exact structure and transformations that shaped your production data, guaranteeing consistency across environments.

YouTube video in which nuno talks about seeders

Another developer explained why he always uses migrations for default values:

  • They support dependencies between tables.

  • Easier to revert a release by using down() to remove the inserted data.

  • Avoids the risk of someone forgetting to run the seeders, or accidentally running test seeders in production.

On the other hand, many developers say:

“Migrations should migrate schema, not seed data.”

But they often don’t address the fact that default values are actually part of the schema of the application.


My case

In my project I have two different types of default data:

  1. Roles (user, admin): These feel like critical system data, so I’m leaning toward adding them in a migration.

  2. DishCategories & DishSubcategories: Here it gets tricky. I have a large predefined array of categories and subcategories (kind of like a starter set). To avoid bloating the migration file, I moved that array into a separate PHP file and include it when inserting the data.

Now I’m wondering: is this an acceptable approach?

The important note

On the one hand, these are not “test” values, they are part of the actual application logic (used in search). On the other hand, they can evolve later by being managed in the admin panel, so maybe seeders make more sense here.


Automated testing concerns

Laravel Daily also mentioned a possible issue when putting data population inside migrations:

  • During automated tests, when migrations are re-run, the default data might get inserted multiple times unless you use insertOrIgnore or similar safeguards.

  • This could lead to duplicate rows in test environments.

YouTube video in which Laravel Daily talks about those 2 approaches


A code example of these 2 approaches (Seeder / Migration)

Seeder

class RolesSeeder extends Seeder
{
    public function run(): void
    {
        Role::create(['name' => 'user']);
        Role::create(['name' => 'admin']);
    }
}

Migration

    public function up(): void
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->timestamps();
        });

        $roles = [
            'user',
            'admin'
        ];

        foreach ($roles as $role){
            DB::table('roles')->insert([
                'name' => $role,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }
    }

Would be grateful to see your thoughts about seeding poroduction data and see the proper practicies

Best regards

0 likes
11 replies
Snapey's avatar

I have done both, and in most cases its not worth the thought process.

How often do you need to deploy to a blank database?

You don't mention running a command, which is also a valid option.

Using Spatie permissions I use a command to setup default roles and permissions. I ensure that the command is idempotent meaning that it can be run multiple times with no adverse effects. In it I have the default values, and after the command I can be sure that all the defaults are represented in the database.

One month later, when the database has production data, and I need to add a new permission, I add it to the defaults in my command and run the command again as part of my deployment script.

1 like
dmytroshved's avatar

@Snapey Yeah, roles situation is clear. But what about list of categories? In which place it will be more properly to place? Seeders? Migrations? Artisan command (SeedCategoriesCommand)?

dmytroshved's avatar

@Snapey

How often do you need to deploy to a blank database?

Its my first time deploying something, so I cant answer that question. Or answer depends on my choice to use migrations or seeders?

Glukinho's avatar

There is a special package for it: https://github.com/TimoKoerber/laravel-one-time-operations

It's like "migrations for data" with tracking that an operation is executed only once.

But I'm happy with simple command app:seed-initial-data which is executed several times during developing and one time at the moment the app is deployed to prod.

1 like
martinbean's avatar

@dmytro_shved If you’re populating tables with initial records (that subsequent migrations may rely on existing) then you should be creating those records in migrations, not seeders. You can’t stop a migration run at an arbitrary point in order to go off and run a separate seeder command.

So if you’re say, creating a statuses table, then you can insert the initial values after creating the table:

public function up(): void
{
    Schema::create('statuses', function (Blueprint $table): void {
        $table->id();
        $table->string('name')->unique();
        $table->timestamps();
    });

    DB::table('statuses')->insert([
        ['name' => 'pending'],
        ['name' => 'submitted'],
        ['name' => 'complete'],
        ['name' => 'cancelled'],
    ]);
}

Use seeders for populating a database with test records; not for production data.

1 like
dmytroshved's avatar

@martinbean So, for example, if I have a migration with roles and then after the roles a user is created (whose default role is user, then is it better for me to put everything in the migration?)

roles migration

    public function up(): void
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->timestamps();
        });

        $roles = [
            'user',
            'admin'
        ];

        foreach ($roles as $role){
            DB::table('roles')->insert([
                'name' => $role,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }
    }

users migration

        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
            $table->foreignId('role_id')->default(1)->constrained('roles', 'id')->cascadeOnDelete();
            $table->string('photo')->default('user_logo/default-image.png');
        });
dmytroshved's avatar

@martinbean And would like to see your opinion about putting the seeder inside migration, I took that approach from Laravel Daily

Quite often you need to create a new DB table and immediately seed it with some data. But in production environment you can't just run "artisan db:seed", especially if you have automated deployment setup which involves only "artisan migrate" command.

The trick is to launch a specific seeder from migration file itself.

public function up()
{
    Schema::create('themes', function (Blueprint $table) {
        $table->increments('id');
        $table->text('name');
    });

    Artisan::call('db:seed', [
        '--class' => ThemesTableSeeder::class
    ]);
}

What do you think?

ctrlaltdelme's avatar

So if I have permissions I need to create, would that be done in a migration or seeder?

dmytroshved's avatar

I am not sure what you mean about permissions, if you mean roles, so yeah, put it inside migrations:

create_roles_table

    public function up(): void
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->timestamps();
        });

        $roles = [
            'user',
            'admin'
        ];

        foreach ($roles as $role){
            DB::table('roles')->insert([
                'name' => $role,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        }
    }

Use Seeders for local development

Use Migrations to populate production data

Like so

jlrdw's avatar

For production, just use the keyboard to enter actual data.

Please or to participate in this conversation.