aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Web Development

Laravel Migration and Seeder: Schema and Test Data

A Laravel migration is how you version your database schema in exactly the same way you version your code. Instead of opening tables by hand in a SQL client, you describe every schema change in a PHP class; that class lets everyone on your team and your server build the same structure with a single command. A seeder is the official way to fill that schema with realistic test data. Use the two together and spinning up the project from scratch shrinks to one line, while most "it worked on my machine" problems simply disappear.

What a migration is and why it beats hand-written SQL

Writing schema by hand has two big problems: no change history is kept, and everyone's database ends up slightly different. Migrations solve both. Each file is named with a timestamp so the order is unambiguous; the up() method applies the change and the down() method reverses it. Because migrations live in Git, your schema history moves forward together with your commit history.

  • Repeatable: the same command builds the same structure on every machine.
  • Reversible: you can roll a bad change back with down().
  • Trackable: the migrations table records which file ran, so it never runs twice.

Creating your first migration

The Artisan command generates both the file and its skeleton. Use --create for a brand-new table and --table to alter an existing one:

php artisan make:migration create_posts_table

# To add a column to an existing table:
php artisan make:migration add_status_to_posts_table --table=posts

The generated file lands under database/migrations/. A create_posts_table looks like this:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('body');
            $table->boolean('published')->default(false);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

$table->id() creates an auto-incrementing primary key, and timestamps() adds the created_at and updated_at columns. There is a fluent API for most column types: string, text, integer, boolean, json, decimal and many more.

Running and rolling back migrations

To apply every pending migration:

php artisan migrate

These are the commands you will reach for most during development:

  • php artisan migrate:rollback — undoes the last batch of migrations.
  • php artisan migrate:rollback --step=2 — rewinds the last two steps.
  • php artisan migrate:fresh — drops all tables and rebuilds from scratch.
  • php artisan migrate:refresh — rolls everything back and runs it again.
  • php artisan migrate:status — lists which migrations have run.

A word of caution in production: migrate:fresh and refresh destroy data. On a live server use only php artisan migrate --force; the --force flag lets it run in production without asking for confirmation.

Relationships and indexes

You connect tables with foreign keys, and Laravel offers a short helper for them:

Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('post_id')
          ->constrained()
          ->cascadeOnDelete();
    $table->text('body');
    $table->timestamps();
});

foreignId('post_id')->constrained() automatically creates a foreign key that references the id column of the posts table. cascadeOnDelete() makes sure that when a post is deleted, its comments go with it. Don't forget to add an index to frequently queried columns with $table->index('slug'); on large tables this dramatically improves read speed.

Generating test data with a seeder

Once the schema is ready, you write a seeder to fill it. Artisan generates the seeder class too:

php artisan make:seeder PostSeeder

The class lands under database/seeders/ and inserts records inside its run() method:

<?php

namespace Database\Seeders;

use App\Models\Post;
use Illuminate\Database\Seeder;

class PostSeeder extends Seeder
{
    public function run(): void
    {
        Post::create([
            'title'     => 'Hello world',
            'slug'      => 'hello-world',
            'body'      => 'The body of the first post.',
            'published' => true,
        ]);
    }
}

To run a single seeder, pass its class name:

php artisan db:seed --class=PostSeeder

To chain all seeders, use call() inside DatabaseSeeder; php artisan db:seed runs that main class:

public function run(): void
{
    $this->call([
        UserSeeder::class,
        PostSeeder::class,
        CommentSeeder::class,
    ]);
}

Realistic, bulk data with factories

Writing a few rows by hand is fine for small projects, but real testing wants hundreds of records. This is where the factory comes in: it describes how a single record is generated and lets the Faker library handle the rest.

php artisan make:factory PostFactory --model=Post
public function definition(): array
{
    return [
        'title'     => fake()->sentence(),
        'slug'      => fake()->unique()->slug(),
        'body'      => fake()->paragraphs(3, true),
        'published' => fake()->boolean(80),
    ];
}

Then a single line in the seeder produces 50 realistic posts:

Post::factory()->count(50)->create();

You can chain related data through factories too: Post::factory()->count(10)->hasComments(5)->create() creates five comments for every post. This approach feeds demos and automated tests with consistent yet varied data.

A practical workflow and common mistakes

In day-to-day development, the cleanest reset rebuilds schema and data in one command:

php artisan migrate:fresh --seed

A few practical rules will save you trouble:

  • Once a migration has run and been committed, do not edit it — write a new one. Editing only takes effect on machines that haven't run it yet.
  • Always fill the down() method to be the exact inverse of up(), and actually test that rolling back works.
  • Using updateOrCreate in seeders prevents duplicate records when you run the same seeder a second time.
  • Never hardcode production data in a seeder; apart from fixed reference data (countries, roles), seeders are for testing and development.

Frequently Asked Questions

What is the difference between a migration and a seeder?

A migration defines and versions the structure of the database (tables, columns, indexes, relationships). A seeder inserts data into that structure. One builds the skeleton, the other fills it with test or sample records. They serve different purposes but work together.

Will running migrations on a live server delete data?

php artisan migrate only applies pending new migrations and never touches existing data. The commands that destroy data are migrate:fresh and migrate:refresh; do not use them in production. On a live server always prefer migrate --force.

How do I add a column later on?

Don't edit the old migration. Create a new one with make:migration add_x_to_y_table --table=y, add the column in up() with something like $table->string('x')->nullable(), reverse it in down() with $table->dropColumn('x'), then run php artisan migrate.

Want a clean, testable database layer in your project? I design and implement Laravel migration, seeder and factory structures tailored to what your project needs. To talk through the details, get in touch with me.

Devamı için