DEV Community

Vincent Tommi
Vincent Tommi

Posted on

Laravel Database Migrations: 5 Mistakes I Learned to Avoid When Moving from Django

When transitioning from Django to Laravel, one of the biggest lessons is understanding how database migrations work and, more importantly, how Laravel tracks them.

Recently, while working on a Laravel POS application, I encountered a migration issue involving a warehouse table. What initially looked like a small PHP syntax problem revealed a few important lessons about migration history, existing databases, and safe schema changes.

I'm sharing this experience for developers who are learning Laravel, especially those coming from Django or another framework.

  1. The problem: Markdown accidentally ended up inside a PHP file

I inspected my original warehouse migration:

database/migrations/2022_03_02_125151_create_warehouse_table.php
Enter fullscreen mode Exit fullscreen mode

The beginning of the file looked like this:

Enter fullscreen mode Exit fullscreen mode


php
<?php

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

The problem was the extra ```

`php` before the PHP opening tag.

This can happen when copying code from a Markdown tutorial, documentation page, or AI-generated response into a source file without removing the Markdown formatting.

A PHP file should start with:



```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
Enter fullscreen mode Exit fullscreen mode

The Markdown fence belongs in documentation, not inside the PHP source file.

Lesson: Always distinguish between code and the formatting used to display that code. A Markdown code block and a PHP source file are not the same thing.

  1. Understanding why editing an old migration didn't update my database

The warehouse migration was originally created in 2022. My application had already run it, and Laravel showed it as Ran when I executed:

php artisan migrate:status
Enter fullscreen mode Exit fullscreen mode

This is where understanding Laravel's migration system becomes important.

Laravel records executed migrations in a database table called migrations. When you run:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

Laravel checks which migration files have not yet been recorded as executed and runs those pending migrations.

It does not normally rerun a migration just because you edited its PHP file.

For example, suppose the original migration creates a warehouse table:

Schema::create('warehouses', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->timestamps();
});
Enter fullscreen mode Exit fullscreen mode

Later, you edit the same migration to add new fields:

$table->string('business_type')->default('retail');
$table->decimal('service_charge_pct', 5, 2)->default(0);
$table->string('default_dine_type')->nullable();
Enter fullscreen mode Exit fullscreen mode

If the original migration has already run, editing the file does not automatically add those columns to the existing database.

This was one of the key lessons from my debugging process.

  1. The correct approach: Create a new migration

For an existing application, the recommended approach is to create a new migration for schema changes.

For example:

php artisan make:migration add_business_settings_to_warehouses_table --table=warehouses
Enter fullscreen mode Exit fullscreen mode

Laravel generates a new migration file in database/migrations.

If the three columns are genuinely missing, the migration could look 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::table('warehouses', function (Blueprint $table) {
            $table->string('business_type')
                ->default('retail');

            $table->decimal('service_charge_pct', 5, 2)
                ->default(0);

            $table->string('default_dine_type')
                ->nullable();
        });
    }

    public function down(): void
    {
        Schema::table('warehouses', function (Blueprint $table) {
            $table->dropColumn([
                'business_type',
                'service_charge_pct',
                'default_dine_type',
            ]);
        });
    }
};
Enter fullscreen mode Exit fullscreen mode

The important detail is that this migration should only be used if the columns do not already exist.

In my case, I later checked the actual database and discovered that all three columns were already present. Therefore, I did not need to create another migration to add them.

Never assume that a column is missing just because you edited a migration file. Inspect the actual database first.

  1. Verify the actual database schema

Coming from Django, you might be familiar with commands such as:

python manage.py showmigrations
Enter fullscreen mode Exit fullscreen mode

Laravel provides a similar migration-status command:

python manage.py showmigrations
Enter fullscreen mode Exit fullscreen mode

Laravel provides a similar migration-status command:

php artisan migrate:status
Enter fullscreen mode Exit fullscreen mode

This shows which migrations have run and which are pending.

However, migration status does not tell you every column currently present in a table. To inspect the actual schema, I used Laravel Tinker:

php artisan tinker
Enter fullscreen mode Exit fullscreen mode

Then:

Schema::getColumnListing('warehouses');
Enter fullscreen mode Exit fullscreen mode

My output was:

[
    "id",
    "name",
    "phone",
    "country",
    "city",
    "email",
    "zip_code",
    "business_type",
    "service_charge_pct",
    "default_dine_type",
    "created_at",
    "updated_at",
]

Enter fullscreen mode Exit fullscreen mode

This confirmed that the three business settings columns already existed.

For Django developers, think of this as inspecting the database directly rather than relying only on the migration files or migration history.

The migration files describe intended schema changes; the database inspection tells you what is actually there.

  1. Avoid destructive fixes on an existing database

When a migration problem occurs, it can be tempting to reset everything.

For example:

php artisan migrate:fresh

This command drops all tables and runs migrations again.

It is useful in disposable development databases, but it is dangerous when you have data you need to preserve.

Similarly, rolling back an old migration just to apply a new column can have unintended consequences, especially if the migration drops a table containing existing data.

For an existing application, a safer process is:

1 Inspect the migration file.

2 Check php artisan migrate:status.

3 Inspect the actual table columns.

4 Create a new migration only for genuinely missing changes.

5 Back up important data before applying schema changes.

6 Run the migration and verify the result.

7 Never use a destructive reset as your first debugging step.

6. Laravel vs. Django: What Should Transitioning Developers Remember?

Concept Django Laravel
Create a migration python manage.py makemigrations php artisan make:migration ...
Apply migrations python manage.py migrate php artisan migrate
Check migration history python manage.py showmigrations php artisan migrate:status
Open an interactive shell python manage.py shell php artisan tinker
Migration tracking django_migrations migrations
Add a column to an existing table Create and apply a new migration Create and apply a new migration

The commands differ, but the core principle is similar: migrations are versioned schema changes, and each framework tracks which changes have been applied.

The critical habit is to understand the difference between editing an existing migration file and creating and applying a new database migration.

The commands differ, but the core principle is similar: migrations are versioned schema changes, and the framework tracks which changes have been applied.

The critical habit is to understand the difference between editing a migration file and applying a new database migration.

  1. My final checklist for Laravel migrations

Before changing a migration in an existing project, I now recommend asking these questions:

  • Does the migration file contain valid PHP, without Markdown fences or copied formatting?

  • Has the migration already run?

  • Does the column actually exist in the database?

  • Would a new migration be safer than editing the original?

  • Could the migration create duplicate columns or tables?

  • Could rolling back or resetting the database destroy important data?

These checks are simple, but they can prevent unnecessary debugging and accidental data loss.

Conclusion

Moving from Django to Laravel involves more than learning new syntax and Artisan commands. It also means understanding Laravel's conventions and how it tracks database changes.

My warehouse migration issue taught me three practical lessons:

  • Keep Markdown formatting out of source files.

  • Do not expect Laravel to rerun an already-applied migration after you edit it.

  • Inspect the actual database before creating migrations or attempting destructive fixes.

  • If you're also transitioning to Laravel, I hope this real-world debugging experience saves you some time.

What migration mistakes have you encountered while learning Laravel? Share them in the comments so other developers can learn from your experience.
Have I verified the schema after applying the change?

These checks are simple, but they can prevent unnecessary debugging and accidental data loss.

Top comments (0)