一、数据库迁移的那些事儿

咱们程序员最怕什么?改需求排第二,数据库迁移绝对能排第一。每次上线前搞数据迁移,那感觉就像拆炸弹——剪红线还是蓝线?今天咱们就好好聊聊PHP里的数据库迁移,怎么用版本控制管住这些"炸弹",怎么优雅地回滚,还有怎么在不同环境里安全玩耍。(技术栈:PHP + Laravel框架 + MySQL)

先看个最简单的迁移文件例子:

// database/migrations/2023_10_01_000000_create_users_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration
{
    // 执行迁移
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->timestamps();  // 自动创建created_at和updated_at字段
        });
    }

    // 回滚迁移
    public function down()
    {
        Schema::dropIfExists('users');  // 删除表
    }
}

二、迁移文件版本控制实战

版本控制不是git的专利,数据库迁移更需要版本控制。Laravel的做法很聪明——用时间戳当文件名前缀,这样既保证顺序,又能避免冲突。

来看看团队协作时常见的场景:小张和小王同时创建迁移文件怎么办?看这个例子:

// 小张创建的迁移文件(下午3点)
// database/migrations/2023_10_01_150000_add_avatar_to_users_table.php

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('avatar')->after('email')->nullable();
    });
}

// 小王创建的迁移文件(下午4点)  
// database/migrations/2023_10_01_160000_add_status_to_users_table.php

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->tinyInteger('status')->after('avatar')->default(1);
    });
}

这里有个坑要注意:如果小王的迁移先执行就会报错,因为avatar字段还不存在!解决方法有两个:

  1. 调整迁移文件名的时间戳顺序
  2. 使用->after()时确保引用的字段已存在

三、回滚机制的花式玩法

回滚不是简单的"撤销",要考虑数据安全。Laravel提供了几种回滚方式:

  1. 普通回滚(回滚上一个迁移):
php artisan migrate:rollback
  1. 回滚指定步数:
php artisan migrate:rollback --step=3
  1. 完全重置(慎用!):
php artisan migrate:reset

高级玩法是自定义回滚逻辑。比如用户表里有重要数据,直接删除表会出大事:

public function down()
{
    // 安全回滚方案:先备份再删除
    DB::statement('CREATE TABLE users_backup AS SELECT * FROM users');
    Schema::dropIfExists('users');
}

更专业的做法是使用事务:

public function up()
{
    DB::beginTransaction();
    try {
        Schema::table('users', function (Blueprint $table) {
            $table->decimal('balance', 10, 2)->default(0);
        });
        DB::commit();
    } catch (\Exception $e) {
        DB::rollBack();
        throw $e;
    }
}

四、多环境迁移策略精要

开发、测试、生产环境配置不同?这几个技巧帮你搞定:

  1. 环境判断执行:
public function up()
{
    if (app()->environment('production')) {
        // 生产环境特殊处理
        $this->productionChanges();
    } else {
        // 开发环境处理
        $this->devChanges();
    }
}
  1. 使用种子数据:
// 在迁移中调用Seeder
public function up()
{
    Schema::table('products', function (Blueprint $table) {
        // 添加字段...
    });
    
    if (app()->environment('local')) {
        Artisan::call('db:seed', [
            '--class' => 'ProductDemoSeeder'
        ]);
    }
}
  1. 多数据库连接处理:
// config/database.php
'connections' => [
    'user_db' => [...],
    'order_db' => [...]
];

// 迁移文件指定连接
public function __construct()
{
    $this->connection = 'user_db';
}

五、避坑指南与高级技巧

  1. 字段修改的坑:Laravel的change()方法需要doctrine/dbal包
composer require doctrine/dbal
  1. 长事务处理:大数据量迁移要分批
public function up()
{
    User::chunk(1000, function ($users) {
        foreach ($users as $user) {
            $user->update(['new_column' => 'default']);
        }
    });
}
  1. 迁移性能优化
// 禁用索引更新加速大批量插入
Schema::disableForeignKeyConstraints();
// 执行迁移...
Schema::enableForeignKeyConstraints();

六、实战:电商系统迁移案例

假设我们要给电商系统增加优惠券功能:

// 2023_10_01_170000_create_coupons_table.php
public function up()
{
    Schema::create('coupons', function (Blueprint $table) {
        $table->id();
        $table->string('code')->unique();
        $table->decimal('discount', 8, 2);
        $table->dateTime('expires_at');
        $table->timestamps();
    });

    // 用户-优惠券关联表(多对多)
    Schema::create('user_coupon', function (Blueprint $table) {
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->foreignId('coupon_id')->constrained()->cascadeOnDelete();
        $table->primary(['user_id', 'coupon_id']);
    });
}

回滚时要特别注意外键约束:

public function down()
{
    Schema::table('user_coupon', function (Blueprint $table) {
        $table->dropForeign(['user_id']);
        $table->dropForeign(['coupon_id']);
    });
    
    Schema::dropIfExists('user_coupon');
    Schema::dropIfExists('coupons');
}

七、技术选型与替代方案

虽然本文以Laravel为例,但其他框架也有类似方案:

  1. Phinx:纯PHP迁移工具,不依赖框架
// phinx迁移示例
$table = $this->table('users');
$table->addColumn('avatar', 'string', ['null' => true])
      ->update();
  1. Doctrine Migrations:适合Symfony项目

  2. 原生SQL迁移:简单粗暴但难维护

-- up.sql
ALTER TABLE users ADD COLUMN avatar VARCHAR(255) NULL AFTER email;

-- down.sql
ALTER TABLE users DROP COLUMN avatar;

八、总结与最佳实践

经过这一通折腾,我总结出几条血泪经验:

  1. 小步快跑:每个迁移文件只做一件事
  2. 严格排序:字段添加要在表创建之后
  3. 环境隔离:测试环境的种子数据别跑到生产去
  4. 备份为王:重大变更前手动备份
  5. 文档同行:在迁移文件里写清楚变更原因

最后送大家一个迁移检查清单: ✓ 测试过回滚吗? ✓ 多环境测试了吗? ✓ 有依赖的迁移顺序对吗? ✓ 大表操作有性能优化吗? ✓ 团队其他成员知道怎么用吗?

记住,好的数据库迁移就像好的版本控制——让人感觉不到它的存在,但关键时刻从不掉链子。