Database Migrations Best Practices: Schema & Data Integrity
Master database migrations by writing reversible schema changes, handling complex data migrations, and managing foreign key constraints in team environments.
Previously in this course, we discussed Database Transactions for Data Integrity in Laravel, which ensures that your application logic remains consistent during multi-step updates. While transactions protect your data at runtime, database migrations protect your application's evolution over time.
In a professional team environment, the database is the shared source of truth. If your migration strategy is fragile, you aren't just slowing down development—you're risking production outages.
Mastering Database Migrations in a Team Environment
Migrations are version control for your database. When multiple developers are working on the same project board, running php artisan migrate must be a predictable, idempotent process.
1. Writing Reversible Migrations
Every migration must have a corresponding down() method. If a deployment fails, you need to be able to roll back to the previous stable state instantly.
Avoid using DB::statement() for complex schema changes when the Schema builder provides native support. Native methods are more likely to be driver-agnostic and easier to reverse.
PHP#6A9955">// Good: Reversible public function up() { Schema::table('tasks', function (Blueprint $table) { $table->string('status')->default('pending'); }); } public function down() { Schema::table('tasks', function (Blueprint $table) { $table->dropColumn('status'); }); }
2. Handling Data Migrations
Sometimes, a schema change requires a structural shift that involves existing data. For example, if you are splitting a name column into first_name and last_name, you cannot simply drop the old column immediately.
The pattern here is:
- Add the new columns (nullable).
- Migrate the data (usually via a separate migration or a temporary command).
- Validate the data.
- Drop the old column in a subsequent deployment.
Never perform heavy data manipulation inside a Schema migration if it involves thousands of rows; it will time out your deployment. Use a queued job or a custom Artisan command for the data movement.
3. Managing Foreign Key Constraints
Foreign keys are your last line of defense against corrupted data. Always define them, but be explicit about your onDelete behavior. Using cascade is convenient, but restrict or nullOnDelete is often safer for business-critical data like project tasks.
PHPSchema::table('tasks', function (Blueprint $table) { $table->foreignId('project_id') ->constrained() ->onDelete('restrict'); #6A9955">// Prevents deleting a project with active tasks });
Worked Example: Evolving the Project Board
In our running project, let's say we need to add a priority level to our tasks table. We need to ensure that existing tasks receive a default value without breaking the database.
- Create the migration:
php artisan make:migration add_priority_to_tasks_table --table=tasks - Define the schema change:
PHPpublic function up() { Schema::table('tasks', function (Blueprint $table) { #6A9955">// Add as nullable first, or provide a default $table->unsignedTinyInteger('priority')->default(1); }); } public function down() { Schema::table('tasks', function (Blueprint $table) { $table->dropColumn('priority'); }); }
Hands-on Exercise
In your project board, create a new migration to add a due_date column to the tasks table.
- Use the
timestampcolumn type. - Ensure the migration is reversible.
- Add a foreign key constraint to link tasks to a
category_id(assuming you have a categories table), usingonDelete('cascade'). - Run
php artisan migrate, thenphp artisan migrate:rollbackto verify yourdown()method works as expected.
Common Pitfalls
- Modifying existing migrations: Never edit a migration file that has already been pushed to the repository. Once it's in production, it's immutable. Create a new migration to fix the issue.
- Ignoring the
down()method: If you leave it empty, you lose the ability to recover from a bad migration. - Hardcoded environment logic: Avoid using
env()inside migrations. Use configuration files or default values; migrations should be environment-agnostic. - Assuming local state: Always assume your migration will run on a database that has different data than yours. Use
nullable()or default values to avoid "Column cannot be null" errors.
Recap
Effective database management requires treating migrations as production-grade code. By writing reversible schema changes, handling data migrations as distinct steps, and enforcing schema integrity with foreign keys, you ensure that your team can deploy with confidence.
Up next: Job Chaining and Batching, where we will handle complex, multi-step asynchronous workflows for our project board.
Work with me

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.