数据库迁移与种子:版本控制你的数据库
什么是数据库迁移与种子
数据库迁移(Migration)和种子(Seed)是现代应用开发中管理数据库结构变更和初始数据的核心机制。它们将数据库的演进纳入版本控制,使团队协作变得安全、可重复且可追踪。
- 迁移(Migration):通过代码定义数据库结构的变更(如创建表、添加列、修改索引),而不是直接操作数据库服务器。每一次变更都记录为一个带时间戳的脚本文件,可以向前应用(
up)或向后回滚(down)。 - 种子(Seed):用于向数据库填充应用程序运行所需的初始数据或测试数据,例如默认管理员账号、国家列表、分类目录等。种子通常在迁移完成之后执行。
把迁移与种子引入你的项目,意味着数据库的“状态”变成了代码,可以被提交到 Git,在任何环境下精确复现。
为什么需要迁移与种子
传统方式带来的问题
在没有迁移工具时,开发者经常需要:
- 手动导出 SQL 转储,再导入另一环境,极易出现表结构不同步。
- 在团队中通过邮件或聊天软件传递 SQL 脚本,版本混乱,难以知道哪条语句已执行。
- 生产环境与开发环境出现“它在我机器上能跑”的结构性差异。
- 无法快速重置或销毁数据库结构,测试成本高。
迁移与种子带来的优势
- 版本控制一致性:数据库的结构变更与应用程序代码放在同一个仓库中,检出任意版本即可获得对应的数据库结构。
- 可重复部署:在新环境重建数据库时,只需运行迁移命令,再执行种子即可得到完全相同的基线。
- 团队协作安全:每个开发者提交迁移文件,经过代码审查后合并,确保改动清晰可控。
- 回滚能力:多数迁移框架支持
down方法,可以撤销最近的变更,快速应对上线问题。 - 测试隔离:可以在测试框架中自动构建并销毁数据库,保证每次测试都在干净的数据集上运行。
迁移文件的基本结构
以最常见的 Laravel(PHP)风格为例,一个迁移类包含 up 和 down 两个方法:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePostsTable extends Migration
{
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('posts');
}
}
其他框架(如 Django、TypeORM、Prisma、Entity Framework)的迁移原理非常相似,都是将数据库操作抽象成编程语言方法,而非手写原生 SQL。
迁移文件的命名约定
迁移文件通常包含时间戳,以确保执行顺序唯一:
2025_01_01_120000_create_posts_table.php
2025_01_02_093000_add_is_published_to_posts.php
当运行迁移命令时,框架会按时间戳顺序执行尚未运行过的文件,并在数据库中记录已执行的迁移日志(通常存放在 migrations 表中)。
迁移的常用操作
下面展示日常开发中最频繁使用的迁移操作,并强调最佳实践。
创建表与字段类型
Schema::create('products', function (Blueprint $table) {
$table->id(); // 自增主键
$table->string('name', 150); // 带长度限制的字符串
$table->decimal('price', 8, 2); // 精确小数
$table->text('description'); // 长文本
$table->boolean('is_active')->default(true);
$table->timestamps(); // created_at 和 updated_at
});
修改现有表
使用独立的迁移文件修改表,绝不要回退到旧迁移中去修改,这会破坏其他环境的迁移链。
Schema::table('products', function (Blueprint $table) {
$table->string('sku')->nullable()->after('name');
$table->integer('stock')->default(0);
});
添加索引与外键
// 普通索引
$table->index('sku');
// 唯一索引
$table->unique('email');
// 外键约束(注意指定引用表和 onDelete 行为)
$table->foreignId('user_id')
->constrained()
->onDelete('cascade');
重命名列或删除列
// 重命名:需要安装 doctrine/dbal 包
$table->renameColumn('title', 'post_title');
// 删除列
$table->dropColumn('old_field');
遵循“每次只做一件事”的原则,让每个迁移文件只负责一个明确的结构变更,便于审查和回滚。
运行与回滚迁移
不同框架的命令行工具略有不同,但核心逻辑一致。
Laravel
# 运行所有未执行的迁移
php artisan migrate
# 回滚最后一个迁移批次(对应当次执行的一组文件)
php artisan migrate:rollback
# 回滚所有迁移
php artisan migrate:reset
# 重建整个数据库(drop 所有表,再执行迁移)
php artisan migrate:fresh
Django
# 创建迁移文件
python manage.py makemigrations
# 执行迁移
python manage.py migrate
# 回滚到某个迁移点
python manage.py migrate app_name 0002_previous_migration
TypeORM / Knex.js / Prisma 等
这些工具同样提供类似的 CLI 命令(如 typeorm migration:run、knex migrate:latest、prisma migrate dev),核心流程均为:创建迁移 → 应用迁移 → 标记已执行。
生产环境的迁移策略
在生产环境执行迁移时:
- 始终先在测试/预发布环境验证迁移。
- 备份数据库。
- 对于可能长时间锁表的操作(如大表增加索引),选择低峰期执行,并考虑使用在线 DDL 工具(如 pt-online-schema-change、gh-ost)。
- 尽量保持向前兼容。例如新增列使用
nullable或提供默认值,避免旧代码因缺少字段而报错。
种子数据(Database Seeding)
种子器用于填充应用程序启动或开发阶段所需的基础数据。它保证了每个环境都有一套一致的基础数据集。
创建种子类
在 Laravel 中,种子类位于 database/seeders/:
use Illuminate\Database\Seeder;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
public function run()
{
User::create([
'name' => 'Admin',
'email' => 'admin@app.com',
'password' => Hash::make('password'),
'role' => 'admin',
]);
// 使用工厂批量生成测试用户
User::factory()->count(50)->create();
}
}
调用多个种子
可以在 DatabaseSeeder 中集中调度:
public function run()
{
$this->call([
UserSeeder::class,
CategorySeeder::class,
TagSeeder::class,
]);
}
运行种子命令:
# Laravel 执行所有未执行的种子
php artisan db:seed
# 重新执行迁移并种子
php artisan migrate:fresh --seed
种子在生产中的使用
生产环境种子通常只包含真正必需的配置数据,如计费套餐、权限角色、国家代码等。这些数据不应通过 UI 手动录入,而是通过种子保证准确性并实现自动化部署。
重要区分:
- 基础/配置数据:通过种子交付,必须纳入版本控制。
- 用户生成内容:绝对不能通过种子覆盖生产数据。
- 测试假数据:在开发/测试环境中使用模型工厂生成,不应出现在生产种子中。
结合版本控制的最佳实践
将迁移文件纳入 Git
database/migrations/(或框架对应的迁移目录)必须提交到版本库。- 永远不要直接修改生产数据库的结构,一切变更都要通过创建新的迁移文件来完成。
养成“迁移+种子”的开发流程
- 创建新分支。
- 生成迁移文件,定义结构变更。
- 编写种子,补充这次变更所需的初始数据(如果需要)。
- 本地执行
migrate fresh --seed验证。 - 提交代码,发起合并请求。
- 在部署流水线中自动执行
migrate命令。
不要生成“修复”迁移
如果团队中的某个迁移有问题,此时应创建一个新的迁移文件来修正错误,而不是修改已提交到主分支的旧迁移。否则其他开发者的数据库会与你产生差异,引发“迁移已应用但文件已变更”的致命问题。
拆分大迁移
如果一张表需要频繁改动,不要反复添加列,而是计划好一次较大的重构迁移,在测试环境充分验证后再合入。
常见框架迁移工具对照
| 框架 / ORM | 迁移工具 | 种子方式 |
|---|---|---|
| Laravel (PHP) | Artisan migrate |
db:seed + Seeder 类 |
| Django (Python) | makemigrations + migrate |
loaddata 或自定义管理命令 |
| Rails (Ruby) | rails db:migrate |
db/seeds.rb |
| TypeORM (TS/JS) | typeorm migration:run |
通常结合工厂自行实现 |
| Knex.js (JS) | knex migrate:latest |
knex seed:run |
| Prisma (TS/JS) | prisma migrate dev |
prisma db seed |
| Entity Framework Core | dotnet ef migrations add |
HasData 方法或自定义 |
虽然命令不同,但思想完全一致:用代码管理数据库的演变历史,用种子注入必要的基础数据。
总结
数据库迁移与种子让你将数据库的“版本”与应用程序代码视为一个整体。通过迁移定义结构变化,通过种子填充初始数据,你的团队将彻底告别手工执行 SQL 的低效与风险。立即开始在你的项目中遵循这一工作流,你将在协作速度、部署安全性和环境重建的便捷性上体会到巨大提升。