本文目录导读:

我来分享一个完整的PHP数据迁移脚本示例,包含多种场景和最佳实践。
基础迁移脚本框架
<?php
/**
* 数据迁移脚本示例
* 用法: php migrate.php [action] [option]
*/
class DatabaseMigration {
private $pdo;
private $config;
private $migrationPath;
private $logFile;
public function __construct($config) {
$this->config = $config;
$this->migrationPath = __DIR__ . '/migrations/';
$this->logFile = __DIR__ . '/migration.log';
try {
$this->pdo = new PDO(
"mysql:host={$config['host']};dbname={$config['database']};charset=utf8mb4",
$config['username'],
$config['password'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
}
/**
* 执行迁移
*/
public function migrate($step = null) {
$this->createMigrationsTable();
$appliedMigrations = $this->getAppliedMigrations();
$migrations = $this->getMigrationFiles();
foreach ($migrations as $migration) {
if (in_array($migration, $appliedMigrations)) {
continue;
}
$result = $this->runMigration($migration);
if ($result['success']) {
$this->log("迁移成功: {$migration}");
$this->recordMigration($migration);
} else {
$this->log("迁移失败: {$migration} - " . $result['error']);
break; // 停止后续迁移
}
if ($step !== null && --$step === 0) {
break;
}
}
echo "迁移完成!\n";
}
/**
* 回滚最近的一次迁移
*/
public function rollback($step = 1) {
$appliedMigrations = $this->getAppliedMigrations();
$migrations = array_reverse($appliedMigrations);
foreach ($migrations as $migration) {
$result = $this->rollbackMigration($migration);
if ($result['success']) {
$this->log("回滚成功: {$migration}");
$this->removeMigrationRecord($migration);
} else {
$this->log("回滚失败: {$migration} - " . $result['error']);
break;
}
if (--$step === 0) break;
}
echo "回滚完成!\n";
}
/**
* 创建迁移记录表
*/
private function createMigrationsTable() {
$sql = "CREATE TABLE IF NOT EXISTS migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
migration_name VARCHAR(255) NOT NULL,
batch INT NOT NULL,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY (migration_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$this->pdo->exec($sql);
}
/**
* 获取已应用的迁移
*/
private function getAppliedMigrations() {
$stmt = $this->pdo->query("SELECT migration_name FROM migrations ORDER BY id");
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
/**
* 获取迁移文件列表
*/
private function getMigrationFiles() {
$files = glob($this->migrationPath . '*.php');
sort($files);
return array_map('basename', $files);
}
/**
* 执行单个迁移
*/
private function runMigration($migration) {
try {
$this->pdo->beginTransaction();
$migrationClass = $this->loadMigration($migration);
$migrationClass->up();
$this->pdo->commit();
return ['success' => true];
} catch (Exception $e) {
$this->pdo->rollBack();
return ['success' => false, 'error' => $e->getMessage()];
}
}
/**
* 回滚单个迁移
*/
private function rollbackMigration($migration) {
try {
$this->pdo->beginTransaction();
$migrationClass = $this->loadMigration($migration);
$migrationClass->down();
$this->pdo->commit();
return ['success' => true];
} catch (Exception $e) {
$this->pdo->rollBack();
return ['success' => false, 'error' => $e->getMessage()];
}
}
/**
* 加载迁移文件
*/
private function loadMigration($migration) {
$file = $this->migrationPath . $migration;
require_once $file;
$className = pathinfo($migration, PATHINFO_FILENAME);
$className = str_replace('_', ' ', $className);
$className = ucwords($className);
$className = str_replace(' ', '', $className);
return new $className($this->pdo);
}
/**
* 记录迁移
*/
private function recordMigration($migration) {
$batch = $this->getLastBatch() + 1;
$stmt = $this->pdo->prepare("INSERT INTO migrations (migration_name, batch) VALUES (?, ?)");
$stmt->execute([$migration, $batch]);
}
/**
* 删除迁移记录
*/
private function removeMigrationRecord($migration) {
$stmt = $this->pdo->prepare("DELETE FROM migrations WHERE migration_name = ?");
$stmt->execute([$migration]);
}
/**
* 获取最后批次
*/
private function getLastBatch() {
$stmt = $this->pdo->query("SELECT COALESCE(MAX(batch), 0) as batch FROM migrations");
$result = $stmt->fetch();
return $result['batch'];
}
/**
* 记录日志
*/
private function log($message) {
$date = date('Y-m-d H:i:s');
$logMessage = "[{$date}] {$message}\n";
file_put_contents($this->logFile, $logMessage, FILE_APPEND);
echo $message . "\n";
}
}
// 配置
$config = [
'host' => 'localhost',
'database' => 'your_database',
'username' => 'your_username',
'password' => 'your_password'
];
// 命令行处理
if (php_sapi_name() === 'cli') {
$action = $argv[1] ?? 'migrate';
$option = $argv[2] ?? null;
$migration = new DatabaseMigration($config);
switch ($action) {
case 'migrate':
$migration->migrate($option ? (int)$option : null);
break;
case 'rollback':
$migration->rollback($option ? (int)$option : 1);
break;
case 'status':
// 可以添加状态查看功能
break;
default:
echo "未知命令\n";
}
}
迁移文件示例
20240101_create_users_table.php
<?php
class CreateUsersTable {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function up() {
$sql = "
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
";
$this->pdo->exec($sql);
}
public function down() {
$this->pdo->exec("DROP TABLE IF EXISTS users");
}
}
数据迁移(不只是结构)
20240102_migrate_user_data.php
<?php
class MigrateUserData {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function up() {
// 示例:从旧表迁移数据到新表
$oldTable = 'old_users';
$newTable = 'users';
try {
// 检查旧表是否存在
$stmt = $this->pdo->query("SHOW TABLES LIKE '{$oldTable}'");
if ($stmt->rowCount() === 0) {
echo "旧表不存在,跳过数据迁移\n";
return;
}
// 批量迁移数据
$offset = 0;
$limit = 1000;
while (true) {
$stmt = $this->pdo->prepare("SELECT * FROM {$oldTable} LIMIT {$limit} OFFSET {$offset}");
$stmt->execute();
$oldRecords = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($oldRecords)) {
break; // 所有数据已迁移
}
foreach ($oldRecords as $record) {
$this->processRecord($record, $newTable);
}
$offset += $limit;
echo "已迁移 {$offset} 条记录...\n";
}
// 可选:删除旧表
// $this->pdo->exec("DROP TABLE IF EXISTS {$oldTable}");
} catch (Exception $e) {
echo "数据迁移失败: " . $e->getMessage() . "\n";
throw $e;
}
}
private function processRecord($record, $table) {
try {
$stmt = $this->pdo->prepare(
"INSERT IGNORE INTO {$table} (id, username, email, password, created_at)
VALUES (?, ?, ?, ?, ?)"
);
$stmt->execute([
$record['id'],
$record['username'],
$record['email'],
$record['password_hash'], // 字段名可能不同
$record['created_at']
]);
} catch (PDOException $e) {
// 跳过重复数据或记录错误
echo "插入记录失败: " . $e->getMessage() . "\n";
}
}
public function down() {
// 回滚操作(如果需要)
echo "数据迁移回滚:无法自动回滚数据迁移\n";
}
}
高级特性示例
<?php
class AdvancedMigration {
private $pdo;
private $startTime;
public function __construct($pdo) {
$this->pdo = $pdo;
$this->startTime = microtime(true);
}
public function up() {
// 1. 添加新列
$this->pdo->exec(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS
phone VARCHAR(20) AFTER email"
);
// 2. 更新现有数据
$stmt = $this->pdo->prepare(
"UPDATE users SET
email = LOWER(email) WHERE email != LOWER(email)"
);
$stmt->execute();
// 3. 创建索引
$this->pdo->exec(
"CREATE INDEX IF NOT EXISTS idx_created_at ON users(created_at)"
);
// 4. 数据清洗
$this->cleanData();
// 输出执行时间
$executionTime = round(microtime(true) - $this->startTime, 2);
echo "执行时间: {$executionTime} 秒\n";
}
private function cleanData() {
// 清洗无效数据
$stmt = $this->pdo->prepare(
"DELETE FROM users WHERE email NOT LIKE '%@%'"
);
$deleted = $stmt->rowCount();
if ($deleted > 0) {
echo "清理了 {$deleted} 条无效数据\n";
}
}
public function down() {
// 回滚操作
$this->pdo->exec("ALTER TABLE users DROP COLUMN IF EXISTS phone");
$this->pdo->exec("DROP INDEX IF EXISTS idx_created_at ON users");
}
}
PHP脚本执行方式
# 执行所有迁移 php migrate.php migrate # 执行特定数量的迁移 php migrate.php migrate 2 # 回滚最近一次的迁移 php migrate.php rollback # 回滚特定数量的迁移 php migrate.php rollback 2
注意事项
- 事务处理: 重要操作使用事务,确保数据一致性
- 分批处理: 大数据量迁移要分批处理,避免内存不足
- 错误处理: 完善的错误处理和日志记录
- 回滚机制: 提供down()方法用于回滚
- 幂等性: 重复执行不会产生问题
- 备份: 生产环境迁移前务必备份数据
- 测试: 在测试环境验证后再执行生产环境迁移
生产环境使用建议
// production_migrate.php
<?php
ini_set('memory_limit', '512M');
set_time_limit(0);
// 安全检查
if (php_sapi_name() !== 'cli') {
die("只能通过命令行执行");
}
// 解析命令行参数
$options = getopt('', ['dry-run', 'force']);
$isDryRun = isset($options['dry-run']);
if (!$isDryRun && !isset($options['force'])) {
echo "生产环境需要确认,请添加 --force 参数\n";
exit(1);
}
// 加载配置
$config = require 'config/database.php';
// 迁移前备份(可选)
if (!$isDryRun) {
echo "执行数据库备份...\n";
system("mysqldump -u{$config['username']} -p{$config['password']} {$config['database']} > backup_" . date('Ymd_His') . ".sql");
}
// 执行迁移
echo $isDryRun ? "DRY RUN 模式\n" : "实际执行模式\n";
// ... 迁移逻辑
这个示例涵盖了大部分数据迁移需求,你可以根据实际情况调整使用。