Understanding Database Migrations

+15 Mana ✨

Introduction

Migrations are version control for your database schema. Every structural change — new table, new column, new index — gets a migration file alongside your code, so every environment stays in sync via php artisan migrate.

Key Concepts

  • Migration file: A timestamped PHP file in database/migrations/ with up() and down() methods.
  • up() / down(): Methods that apply and reverse the schema change. up runs on migrate; down runs on migrate:rollback and migrate:refresh.
  • migrate:fresh vs migrate:refresh: fresh drops all tables and re-runs up (fast, skips down); refresh runs down for every migration then up again.
  • Batch: The grouping used by migrate:rollback to know how far back to go.

Real World Context

Without migrations, every environment drifts. With them, git pull && php artisan migrate keeps dev, staging, and production aligned automatically — and new contributors can spin up a working database in minutes.

Deep Dive

Migrations are like version control for your database schema. They allow your team to define and share the application's database schema definition.

Why Migrations?

Without Migrations

"Hey, I added a column to the users table."
"What's it called? What type?"
"Just run this SQL: ALTER TABLE users ADD COLUMN..."
"Did you update the staging server too?"
🤯

With Migrations

git pull
php artisan migrate
āœ… Database updated!

Migration Benefits

BenefitDescription
Version ControlSchema changes are tracked in Git
Team SyncEveryone has the same database structure
Environment ParityDev, staging, and production stay in sync
RollbackUndo changes if something goes wrong
Database AgnosticSame migrations work on MySQL, PostgreSQL, SQLite

Creating Migrations

bash
# Create a migration
php artisan make:migration create_posts_table

# With model
php artisan make:model Post -m

# For modifying existing table
php artisan make:migration add_views_to_posts_table --table=posts

Migration files are created in database/migrations/:

database/migrations/
ā”œā”€ā”€ 2024_01_01_000000_create_users_table.php
ā”œā”€ā”€ 2024_01_01_000001_create_cache_table.php
ā”œā”€ā”€ 2024_01_15_143022_create_posts_table.php
└── 2024_01_20_091500_add_views_to_posts_table.php

Migration Structure

php
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('body');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Running Migrations

bash
# Run all pending migrations
php artisan migrate

# See status
php artisan migrate:status

# Output:
# Migration name ............................. Batch / Status
# 2024_01_01_000000_create_users_table ............... [1] Ran
# 2024_01_15_143022_create_posts_table ............... Pending

Rolling Back

bash
# Rollback last batch
php artisan migrate:rollback

# Rollback last 3 migrations
php artisan migrate:rollback --step=3

# Rollback all migrations
php artisan migrate:reset

# Rollback all and re-run
php artisan migrate:refresh

# Drop all tables and re-run
php artisan migrate:fresh

# Fresh with seeding
php artisan migrate:fresh --seed

The up() and down() Methods

php
public function up(): void
{
    // Create, modify, or add to the database
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        // ...
    });
}

public function down(): void
{
    // Reverse whatever up() did
    // Should return database to previous state
    Schema::dropIfExists('posts');
}

Why down() Matters

php
// Bad: down() doesn't reverse up()
public function up(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->string('subtitle')->nullable();
        $table->integer('views')->default(0);
    });
}

public function down(): void
{
    // āŒ Missing: doesn't remove the columns!
}

// Good: down() properly reverses up()
public function down(): void
{
    Schema::table('posts', function (Blueprint $table) {
        $table->dropColumn(['subtitle', 'views']);
    });
}

Migration Tips

1. Never Edit Deployed Migrations

php
// āŒ Don't modify migrations that have run in production
// āœ… Create a new migration to make changes

2. Use Descriptive Names

bash
# Good
php artisan make:migration add_published_at_to_posts_table
php artisan make:migration create_post_category_pivot_table
php artisan make:migration drop_legacy_users_table

# Bad
php artisan make:migration update_posts
php artisan make:migration changes

3. Keep Migrations Small

php
// āœ… One logical change per migration
// āŒ Don't combine unrelated changes

Common Pitfalls

  1. Editing a committed migration — Other environments won't re-run it, so the schema diverges. Always write a new migration for changes.
  2. Omitting down() methods — Makes rollbacks impossible or corrupts the schema.
  3. Running migrate:fresh on production — It drops every table. A career-ending command.

Best Practices

  1. Always implement down() — Even if you never plan to roll back, it's a safety net.
  2. Give migrations descriptive names — add_published_at_to_posts_table is self-documenting.
  3. Never mutate a deployed migration — Always create a new one.

Summary

  • Migrations version-control your database schema alongside your code.
  • up() applies changes, down() reverses them.
  • migrate, migrate:rollback, migrate:refresh, migrate:fresh offer different rollback strategies.
  • Treat migrations as append-only after they've been committed and deployed.
āœ“ Completed