一、数据库迁移的那些事儿
咱们程序员最怕什么?改需求排第二,数据库迁移绝对能排第一。每次上线前搞数据迁移,那感觉就像拆炸弹——剪红线还是蓝线?今天咱们就好好聊聊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字段还不存在!解决方法有两个:
- 调整迁移文件名的时间戳顺序
- 使用
->after()时确保引用的字段已存在
三、回滚机制的花式玩法
回滚不是简单的"撤销",要考虑数据安全。Laravel提供了几种回滚方式:
- 普通回滚(回滚上一个迁移):
php artisan migrate:rollback
- 回滚指定步数:
php artisan migrate:rollback --step=3
- 完全重置(慎用!):
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;
}
}
四、多环境迁移策略精要
开发、测试、生产环境配置不同?这几个技巧帮你搞定:
- 环境判断执行:
public function up()
{
if (app()->environment('production')) {
// 生产环境特殊处理
$this->productionChanges();
} else {
// 开发环境处理
$this->devChanges();
}
}
- 使用种子数据:
// 在迁移中调用Seeder
public function up()
{
Schema::table('products', function (Blueprint $table) {
// 添加字段...
});
if (app()->environment('local')) {
Artisan::call('db:seed', [
'--class' => 'ProductDemoSeeder'
]);
}
}
- 多数据库连接处理:
// config/database.php
'connections' => [
'user_db' => [...],
'order_db' => [...]
];
// 迁移文件指定连接
public function __construct()
{
$this->connection = 'user_db';
}
五、避坑指南与高级技巧
- 字段修改的坑:Laravel的change()方法需要doctrine/dbal包
composer require doctrine/dbal
- 长事务处理:大数据量迁移要分批
public function up()
{
User::chunk(1000, function ($users) {
foreach ($users as $user) {
$user->update(['new_column' => 'default']);
}
});
}
- 迁移性能优化:
// 禁用索引更新加速大批量插入
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为例,但其他框架也有类似方案:
- Phinx:纯PHP迁移工具,不依赖框架
// phinx迁移示例
$table = $this->table('users');
$table->addColumn('avatar', 'string', ['null' => true])
->update();
Doctrine Migrations:适合Symfony项目
原生SQL迁移:简单粗暴但难维护
-- up.sql
ALTER TABLE users ADD COLUMN avatar VARCHAR(255) NULL AFTER email;
-- down.sql
ALTER TABLE users DROP COLUMN avatar;
八、总结与最佳实践
经过这一通折腾,我总结出几条血泪经验:
- 小步快跑:每个迁移文件只做一件事
- 严格排序:字段添加要在表创建之后
- 环境隔离:测试环境的种子数据别跑到生产去
- 备份为王:重大变更前手动备份
- 文档同行:在迁移文件里写清楚变更原因
最后送大家一个迁移检查清单: ✓ 测试过回滚吗? ✓ 多环境测试了吗? ✓ 有依赖的迁移顺序对吗? ✓ 大表操作有性能优化吗? ✓ 团队其他成员知道怎么用吗?
记住,好的数据库迁移就像好的版本控制——让人感觉不到它的存在,但关键时刻从不掉链子。
评论