DEV Community

Cover image for The Art of Database Migrations in Laravel -- Common Mistakes
varzoeaa
varzoeaa

Posted on

The Art of Database Migrations in Laravel -- Common Mistakes

What are database migrations in Laravel?

If you've ever worked with databases, you know how tricky it can be to keep track of schema changes—especially when working on a team. That’s where Laravel migrations come in! Think of them as version control for your database—just like Git for your code.

Instead of manually writing SQL commands every time you make a change, migrations allow you to define your database structure in PHP. And the best part? You can roll back changes if needed! 🚀

one

Every migration file in Laravel contains two methods:

  • up() → defines what should happen (e.g., creating tables, adding columns, etc.)
  • down() → reverses the changes (e.g., dropping tables)

Here’s what a basic migration file looks like:

Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('username');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
Enter fullscreen mode Exit fullscreen mode

The Schema builder makes it super easy to define tables and columns in a clean, readable way.

How to handle them efficiently?

Before you start writing migrations, it’s a good idea to plan out your database schema. You can sketch it out on paper, but if you want a digital solution, here are some great tools:

DrawSQL,
Lucidchart,
dbdiagram.io.

Once you have a plan, creating migrations in Laravel is super simple thanks to Artisan, Laravel’s command-line tool. Just run:

php artisan make:migration create_users_table
php artisan migrate
Enter fullscreen mode Exit fullscreen mode

two

What are the best practices?

To keep things clean and manageable, here are some best practices to follow:

✅ Use clear, descriptive names → create_posts_table instead of migration1
✅ Keep migrations small & focused → One change at a time!
✅ Use Laravel’s column modifiers → ->nullable(), ->default(), ->unique() Check out the column modifiers
✅ Stick to Laravel conventions → Table names should be plural (users), while models should be singular (User)

How should I configure my database?

Laravel stores database configuration settings in config/database.php, but you don’t need to touch that much. The real magic happens in your .env file. Here’s what you need to configure:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=my_app
DB_USERNAME=root
DB_PASSWORD=
Enter fullscreen mode Exit fullscreen mode

What are advanced migrations?

Once you’re comfortable with basic migrations, you can take things up a notch with advanced migrations. These include:

🔹 Modifying column types
🔹 Adding database triggers
🔹 Using raw SQL inside migrations

Example: Changing a column type in a migration:

Schema::table('users', function (Blueprint $table) {
    $table->string('username', 100)->change();
});
Enter fullscreen mode Exit fullscreen mode

three

Common Issues and How to Fix Them

1️⃣ Foreign Key Issues: Referencing a Table That Doesn't Exist Yet

If you create a table that references another table before that table exists, Laravel will throw an error.

Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained(); // This will fail if 'users' isn't created yet!
});
Enter fullscreen mode Exit fullscreen mode

Make sure the referenced table is created before you reference it!

✔ Adjust migration order – Ensure the users table migration runs before orders.
✔ Use unsignedBigInteger instead of constrained() and manually define the foreign key:

Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('user_id');
    $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Enter fullscreen mode Exit fullscreen mode

2️⃣ "Badly Constructed Foreign Key Constraint" Error

Foreign key constraints must match the exact data type of the referenced column.

Schema::create('posts', function (Blueprint $table) {
    $table->id();
});

Schema::create('comments', function (Blueprint $table) {
    $table->integer('post_id')->unsigned(); // Wrong data type, should be unsignedBigInteger
    $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
});
Enter fullscreen mode Exit fullscreen mode

✔ Use unsignedBigInteger() for foreign keys instead of integer():
$table->unsignedBigInteger('post_id');
✔ Or an even better approach use the foreignId()
$table->foreignId('post_id')->constrained()->onDelete('cascade');

3️⃣ "Cannot Add Column - Table Already Exists" Error

If you try running php artisan migrate after modifying a migration file without rolling back first, Laravel won’t apply the changes.

✔ If the migration has already run, you can create a new migration for the changes:

php artisan make:migration add_bio_to_users_table

Schema::table('users', function (Blueprint $table) {
    $table->text('bio')->nullable();
});
Enter fullscreen mode Exit fullscreen mode

✔ Rollback the existing migration before modifying
✔ Use php artisan migrate:fresh command with every migration, it drops all tables before migrating again

4️⃣ "Class Not Found" Error When Running Migrations

Laravel sometimes doesn’t recognize migration files, especially after renaming them manually.

✔ Run dump-autoload to refresh class loading.
✔ Run the migrations with the --path option

four

Indexes: Boosting Query Performance

Indexes are super important when working with large datasets. They speed up database queries by allowing the database engine to locate data without scanning the entire table.

When to use indexes?

  • if you frequently query or filter by a column (WHERE email = ?)
  • when sorting (ORDER BY created_at)
  • if you use JOIN queries often

When NOT to use indexes?

  • if the table is small (less than 1,000 rows)
  • on columns with highly unique values (e.g., timestamps)
  • if the column changes frequently (indexes slow down inserts/updates)
$table->string('email')->unique();
$table->unique(['first_name', 'last_name']);
$table->string('category')->index();
$table->text('description')->fullText();
Enter fullscreen mode Exit fullscreen mode

Database Events

Database events allow you to automate certain tasks at the database level. These can be useful for automatically updating timestamps, performing scheduled tasks, logging changes without needing Laravel code.

Using Database Triggers in Laravel

A trigger is an event that fires before or after an action (insert, update, delete).
You can use raw SQL inside a migration to create a trigger:

DB::unprepared('
    CREATE TRIGGER before_user_insert
    BEFORE INSERT ON users
    FOR EACH ROW
    SET NEW.created_at = NOW()
');
Enter fullscreen mode Exit fullscreen mode

Handling Events with Laravel’s Job System

Instead of using raw SQL, Laravel’s event-driven architecture lets you handle database events at the application level.

Example: Dispatching an event when a new user is created:

use App\Events\UserRegistered;

User::created(function ($user) {
    event(new UserRegistered($user));
});
Enter fullscreen mode Exit fullscreen mode

five

Final Thoughts

Migrations are one of the most powerful features in Laravel. Just remember: plan first, write clean migrations, and use Laravel’s built-in tools to make your life easier.

Now go forth and migrate! 🤓🧑‍💻👩‍💻

Top comments (0)