Skip to content

Models & Application Code in Migrations – Good or Bad?

Models and Application code in Migrations - Good or Bad?

In this post, we’re going to take a quick look at whether it’s a good or bad idea to use Eloquent Models and application code in database migrations.

Let’s get started!

What is application code?

In Laravel, the application code is found under the app folder.

There we can find the Controllers, Middlewares, Requests, Services, Repositories, Models, and more.

Basically, they are the ones that tell your app how to work.

What are database migrations?

The database migrations can be found at database/migrations folder.

Migrations are a set of instructions that define the changes you want to make to your database schema.

Do you want to create or drop a table, add or remove columns, or just migrate data from a table to another table?

Then, you should create a migration.

🎥 Prefer a video version of this article instead?

Good or bad idea?

Now coming back to the question of whether it’s a good or bad idea to use Models and application code in database migrations.

The first clue is given to us by the structure of a Laravel project.

The app and migrations folders are separate.

If the migrations folder had been under app, then that would have suggested to us that migrations depend on the business logic.

But that’s not the case.

Laravel follows a rule, namely that migrations should be independent of the application’s code and its state.

Example 1

Let’s see a very simplified example:

<?php

use App\Models\Customers;
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
    public function up(): void
    {
        Customers::factory(3)->create(['status' => Customers::STATUS_PENDING]);
    }
    
    ...
};

In this migration, I’m using a Customers Factory to create 3 customers with the pending status.

Notice that I’m using the constant STATUS_PENDING from the Customers Model.

Run the following command to recreate the database:

php artisan migrate:fresh


  Dropping all tables ... 47ms DONE  

   INFO  Preparing database.

  Creating migration table ... 10ms DONE  

   INFO  Running migrations.

  2024_03_05_153624_create_customers_table ... 5ms DONE  
  2024_03_05_153832_seed_customers_with_status_pending ... 23ms DONE 

The code runs and the customers have been created as expected. ✅

But what if, let’s say, in 5 years from now, we don’t use that status anymore and it was removed from the Model?

Imagine that the above migration is among the hundreds of migrations in the project, and the IDE does not recognize that the constant no longer exists.

Comment out the status of the Customers Model and recreate the database again:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Customers extends Model
{
    use HasFactory;

    // const STATUS_PENDING = 'pending';
}

php artisan migrate:fresh

As expected, we get an error saying that the constant STATUS_PENDING cannot be found.


  Dropping all tables ... 307ms DONE  

   INFO  Preparing database.

  Creating migration table ... 36ms DONE  

   INFO  Running migrations.

  2024_03_05_153624_create_customers_table ... 5ms DONE  
  2024_03_05_153832_seed_customers_with_status_pending ... 395ms FAIL  

   Error 

  Undefined constant App\Models\Customers::STATUS_PENDING

  at database\migrations\2024_03_05_153832_seed_customers_with_status_pending.php:10
      6▕ return new class extends Migration
      7▕ {
      8▕     public function up(): void
      9▕     {
    10         Customers::factory(3)->create(['status' => Customers::STATUS_PENDING]);
     11▕     }
     12▕
     13▕     public function down(): void
     14▕     {

These kinds of changes can be easily overlooked and they can crash the migrations.

Example 2

Let’s take another example.

I don’t have a code to demonstrate this but I’ll ask you to picture the following scenario.

Migration 1 creates a table.

Migration 2 appends the table.

At this point, the Model has been updated to reflect Migration 2, but it’s used by Migration 1.

When Migration 1 is called, the Model will break because Migration 2 has not yet happened.

That means you can be certain of the state of the database, but you cannot be certain of the state of the code.

Instead of using Eloquent Models in migrations, you can use either the Query Builder or the native SQL using DB::statement().

DB::table('users')
            ->where('status', 'active')
            ->orderBy('created_at', 'desc')
            ->get();
DB::statement('ALTER TABLE users ADD COLUMN age INT');

Can we change the old migrations?

Some would say “Yeah, but you can also change the old migrations to reflect the state of the Model“.

While this is technically possible, that means you will alter the structure of the database versioning and you will lose the ability to get back to an older version of the database.

The golden rule is that once a migration has been migrated, its code should not be modified.

If you want to make other database changes, they must be done within a new migration.

Advantage(s) of using Eloquent Models in migrations

Up until this point, I pointed out some of the disadvantages of using Models and application code in migrations.

But what would be the advantage(s) of using Eloquent Models in migrations?

Quite simply, it’s easier to write queries by having access to the full suite of methods and functions that Eloquent provides.

From my point of view, I avoid using Models in migrations.

Some may argue that using them saves time; with the risk of potentially causing issues with the migrations in the future.

But hey, I’d love to hear your opinions too. 💜

Leave a comment below!


Let me know what you think about this article in the comments section below.

If you find this article helpful, please share it with others and subscribe to the blog to support me, and receive a bi-monthly-ish e-mail notification on my latest articles.   
  

Comments