0

Foreign keys — foreignId(), constrained(), onDelete()

Intermediate5 min read·lv-14-006
sqlinterview

Concept

The down() method in a migration should be the exact inverse of up(). Writing it correctly enables safe rollbacks. But in production, rollback strategies require careful thought — some changes are irreversible or dangerous to reverse.

What down() must do: Restore the database to the state it was in before up() ran. If up() creates a table, down() drops it. If up() adds a column, down() drops that column.

Why down() is often wrong:

  • Dropping a column in down() that contains production data loses that data permanently.
  • Rolling back a migration that removed a column would need to recreate it (possibly empty).
  • After deploying to production, rollback may be impossible without data loss.

"No down" philosophy: Some teams only use migrate:rollback in development and never in production. For production fixes, they write a NEW migration that corrects the issue. If down() is never safe to run, leave it empty or throw \RuntimeException('This migration cannot be rolled back safely').

Safe down() practices:

  • Never have down() DROP data that doesn't have a backup.
  • For column removals in up() — backup data first, then down() re-adds an empty column.
  • Always use Schema::dropIfExists() instead of Schema::drop() in down().
  • For complex up() operations, consider omitting down() and adding a comment explaining why.

Schema::table() in down(): When up() adds columns, down() should drop only those specific columns — not the whole table.

Code Example

php
<?php
// Good: Simple additive migration — down() is safe
return new class extends \Illuminate\Database\Migrations\Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('timezone', 50)->nullable()->after('email');
            $table->boolean('newsletter')->default(false)->after('timezone');
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn(['timezone', 'newsletter']);
        });
    }
};

// Be careful: down() that would lose data
return new class extends \Illuminate\Database\Migrations\Migration
{
    public function up(): void
    {
        // Splits 'full_name' into 'first_name' + 'last_name'
        Schema::table('users', function (Blueprint $table) {
            $table->string('first_name')->nullable()->after('id');
            $table->string('last_name')->nullable()->after('first_name');
        });

        // Data migration: populate new columns from old
        DB::table('users')->whereNotNull('full_name')->each(function($user) {
            $parts = explode(' ', $user->full_name, 2);
            DB::table('users')->where('id', $user->id)->update([
                'first_name' => $parts[0],
                'last_name'  => $parts[1] ?? '',
            ]);
        });
    }

    public function down(): void
    {
        // Re-combining data is possible, but column drop loses new data if run post-populate
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn(['first_name', 'last_name']);
        });
        // NOTE: 'full_name' still exists — this down() is relatively safe here
    }
};

// No-rollback migration — explicitly not reversible
return new class extends \Illuminate\Database\Migrations\Migration
{
    public function up(): void
    {
        Schema::table('orders', function (Blueprint $table) {
            $table->dropColumn('legacy_reference_number');
        });
    }

    public function down(): void
    {
        throw new \RuntimeException('This migration drops a column and cannot be safely rolled back.');
    }
};