PHP数据库数据版本管理与变更历史记录维护
在PHP项目开发过程中,数据库结构或初始数据的变更是家常便饭。如果没有规范的版本管理机制,很容易出现不同环境数据库结构不一致、变更无法追溯、回滚困难等问题。本文将介绍一套适用于PHP项目的数据库数据版本管理方案,实现数据库变更的可追溯、可回滚、可同步。
一、核心设计思路
我们的方案核心是通过迁移文件+版本记录表的方式管理数据库变更:
- 每次数据库变更(包括结构变更、数据增减)都生成一个独立的迁移文件,文件命名包含时间戳和变更描述,保证执行顺序
- 数据库中创建一张专门的版本记录表,记录已经执行过的迁移文件信息,避免重复执行
- 提供对应的执行、回滚、状态查询工具,方便日常操作
二、数据库版本记录表设计
首先我们需要在目标数据库中创建一张用于记录迁移历史的表,表结构如下:
-- 创建数据库迁移版本记录表 CREATE TABLE `database_migrations` ( `id` int(11) NOT NULL AUTO_INCREMENT, `migration` varchar(255) NOT NULL COMMENT '迁移文件名', `batch` int(11) NOT NULL COMMENT '批次号,同一批次的迁移属于同一次操作', `executed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '执行时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_migration` (`migration`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库迁移版本记录表';
其中migration字段存储迁移文件的完整文件名,batch字段用于标记同一轮执行的所有迁移,方便批量回滚,executed_at记录迁移执行的时间点。
三、迁移文件规范
迁移文件统一存放在项目的database/migrations目录下,命名规则为时间戳_变更描述.php,例如20240520103000_add_user_table.php。每个迁移文件需要包含up方法和down方法,分别用于执行变更和回滚变更。
下面是一个创建用户表的迁移文件示例:
<?php
/**
* 迁移文件:创建用户表
* 执行时间:2024-05-20 10:30:00
*/
class AddUserTable
{
// 数据库连接实例,实际项目中可通过依赖注入传入
private $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* 执行迁移:创建用户表并插入初始数据
*/
public function up()
{
// 创建用户表
$sql = <<<SQL
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(255) NOT NULL COMMENT '密码哈希',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
UNIQUE KEY `uk_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
SQL;
$this->pdo->exec($sql);
// 插入初始测试用户
$insertSql = "INSERT INTO `users` (`username`, `password`, `email`) VALUES
('admin', '" . password_hash('admin123', PASSWORD_DEFAULT) . "', 'admin@ipipp.com'),
('test', '" . password_hash('test123', PASSWORD_DEFAULT) . "', 'test@ipipp.com')";
$this->pdo->exec($insertSql);
}
/**
* 回滚迁移:删除用户表
*/
public function down()
{
$sql = "DROP TABLE IF EXISTS `users`";
$this->pdo->exec($sql);
}
}四、迁移管理工具实现
接下来我们实现一个简单的迁移管理类,负责读取迁移文件、对比已执行记录、执行迁移或回滚操作。
<?php
/**
* 数据库迁移管理类
*/
class DatabaseMigrator
{
private $pdo;
private $migrationsDir;
private $migrationTable = 'database_migrations';
public function __construct(PDO $pdo, $migrationsDir)
{
$this->pdo = $pdo;
$this->migrationsDir = rtrim($migrationsDir, '/');
}
/**
* 获取所有已执行的迁移文件名
*/
private function getExecutedMigrations()
{
$stmt = $this->pdo->query("SELECT `migration` FROM `{$this->migrationTable}` ORDER BY `id` ASC");
return $stmt->fetchAll(PDO::FETCH_COLUMN) ?: [];
}
/**
* 获取所有待执行的迁移文件
*/
private function getPendingMigrations()
{
$allFiles = scandir($this->migrationsDir);
$migrationFiles = [];
foreach ($allFiles as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'php' && preg_match('/^\d{14}_.*\.php$/', $file)) {
$migrationFiles[] = $file;
}
}
sort($migrationFiles); // 按文件名排序,保证执行顺序
$executed = $this->getExecutedMigrations();
return array_diff($migrationFiles, $executed);
}
/**
* 执行所有待迁移的文件
*/
public function migrate()
{
$pending = $this->getPendingMigrations();
if (empty($pending)) {
echo "没有待执行的迁移文件\n";
return;
}
// 获取当前最大批次号
$stmt = $this->pdo->query("SELECT MAX(`batch`) as max_batch FROM `{$this->migrationTable}`");
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$currentBatch = $result['max_batch'] ? $result['max_batch'] + 1 : 1;
foreach ($pending as $file) {
$filePath = $this->migrationsDir . '/' . $file;
require_once $filePath;
// 提取类名,文件名格式为 时间戳_描述.php,类名取描述部分首字母大写
$className = $this->getClassNameFromFile($file);
if (!class_exists($className)) {
echo "迁移文件 {$file} 未找到对应的类 {$className}\n";
continue;
}
$migration = new $className($this->pdo);
try {
$migration->up();
// 记录执行结果
$stmt = $this->pdo->prepare("INSERT INTO `{$this->migrationTable}` (`migration`, `batch`) VALUES (?, ?)");
$stmt->execute([$file, $currentBatch]);
echo "迁移文件 {$file} 执行成功\n";
} catch (Exception $e) {
echo "迁移文件 {$file} 执行失败:" . $e->getMessage() . "\n";
break;
}
}
}
/**
* 回滚最近一批迁移
*/
public function rollback()
{
// 获取最近一批的迁移
$stmt = $this->pdo->query("SELECT `migration` FROM `{$this->migrationTable}` WHERE `batch` = (SELECT MAX(`batch`) FROM `{$this->migrationTable}`) ORDER BY `id` DESC");
$rollbackMigrations = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (empty($rollbackMigrations)) {
echo "没有可回滚的迁移\n";
return;
}
foreach ($rollbackMigrations as $file) {
$filePath = $this->migrationsDir . '/' . $file;
require_once $filePath;
$className = $this->getClassNameFromFile($file);
if (!class_exists($className)) {
echo "迁移文件 {$file} 未找到对应的类 {$className}\n";
continue;
}
$migration = new $className($this->pdo);
try {
$migration->down();
// 删除执行记录
$stmt = $this->pdo->prepare("DELETE FROM `{$this->migrationTable}` WHERE `migration` = ?");
$stmt->execute([$file]);
echo "迁移文件 {$file} 回滚成功\n";
} catch (Exception $e) {
echo "迁移文件 {$file} 回滚失败:" . $e->getMessage() . "\n";
break;
}
}
}
/**
* 查看迁移状态
*/
public function status()
{
$allFiles = scandir($this->migrationsDir);
$migrationFiles = [];
foreach ($allFiles as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'php' && preg_match('/^\d{14}_.*\.php$/', $file)) {
$migrationFiles[] = $file;
}
}
sort($migrationFiles);
$executed = $this->getExecutedMigrations();
echo "迁移状态列表:\n";
foreach ($migrationFiles as $file) {
$status = in_array($file, $executed) ? '已执行' : '未执行';
echo "[{$status}] {$file}\n";
}
}
/**
* 从文件名中提取类名
*/
private function getClassNameFromFile($fileName)
{
// 文件名格式:20240520103000_add_user_table.php
$namePart = substr($fileName, 15, -4); // 去掉时间戳和.php后缀
$parts = explode('_', $namePart);
$className = '';
foreach ($parts as $part) {
$className .= ucfirst($part);
}
return $className;
}
}五、实际使用示例
在实际项目中,我们可以这样使用迁移管理工具:
<?php
// 数据库连接配置
$dbConfig = [
'host' => '127.0.0.1',
'port' => 3306,
'dbname' => 'test_db',
'username' => 'root',
'password' => 'root123'
];
// 创建PDO连接
$pdo = new PDO(
"mysql:host={$dbConfig['host']};port={$dbConfig['port']};dbname={$dbConfig['dbname']};charset=utf8mb4",
$dbConfig['username'],
$dbConfig['password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// 初始化迁移管理类
$migrator = new DatabaseMigrator($pdo, __DIR__ . '/database/migrations');
// 查看当前迁移状态
echo "===== 迁移状态 =====\n";
$migrator->status();
// 执行所有待迁移文件
echo "\n===== 执行迁移 =====\n";
$migrator->migrate();
// 再次查看状态
echo "\n===== 迁移后状态 =====\n";
$migrator->status();
// 回滚最近一批迁移(如需回滚时执行)
// echo "\n===== 回滚迁移 =====\n";
// $migrator->rollback();六、注意事项
- 迁移文件的
up和down方法必须保证幂等性,多次执行不会导致错误 - 生产环境执行迁移前,建议先在测试环境验证,并且提前备份数据库
- 如果迁移过程中出现异常,需要手动检查数据库状态和版本记录表,避免数据不一致
- 对于已经上线的项目,不要修改已经执行过的迁移文件,新增变更需要创建新的迁移文件
- 如果需要在团队中共享迁移记录,只需要将
database/migrations目录下的文件提交到版本控制系统即可,所有成员拉取代码后执行迁移就能同步数据库结构