PHP 历史数据迁移脚本

wen PHP项目 2

本文目录导读:

PHP 历史数据迁移脚本

  1. 基础数据库迁移脚本
  2. 独立的迁移执行脚本
  3. 带恢复机制的迁移脚本
  4. 命令行迁移工具
  5. 配置文件示例
  6. 使用环境变量
  7. 运行方式
  8. 注意事项

我来提供一个完整的PHP历史数据迁移脚本示例,包含多种常见场景。

基础数据库迁移脚本

<?php
/**
 * PHP数据库历史数据迁移脚本
 */
class DatabaseMigrator {
    private $sourcePDO;
    private $targetPDO;
    private $batchSize = 1000;
    private $logFile;
    public function __construct($sourceConfig, $targetConfig) {
        $this->sourcePDO = $this->createConnection($sourceConfig);
        $this->targetPDO = $this->createConnection($targetConfig);
        $this->logFile = __DIR__ . '/migration_' . date('Y-m-d_H-i-s') . '.log';
    }
    // 创建数据库连接
    private function createConnection($config) {
        $dsn = "mysql:host={$config['host']};dbname={$config['database']};charset=utf8mb4";
        $options = [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ];
        try {
            return new PDO($dsn, $config['username'], $config['password'], $options);
        } catch (PDOException $e) {
            $this->log("连接失败: " . $e->getMessage(), 'ERROR');
            throw $e;
        }
    }
    // 日志记录
    private function log($message, $level = 'INFO') {
        $logEntry = sprintf("[%s] [%s] %s\n", date('Y-m-d H:i:s'), $level, $message);
        file_put_contents($this->logFile, $logEntry, FILE_APPEND);
        echo $logEntry;
    }
    // 单表迁移
    public function migrateTable($tableName, callable $transform = null) {
        try {
            $this->log("开始迁移表: {$tableName}");
            // 获取源表总记录数
            $countQuery = "SELECT COUNT(*) FROM {$tableName}";
            $stmt = $this->sourcePDO->query($countQuery);
            $totalCount = $stmt->fetchColumn();
            $this->log("源表记录总数: {$totalCount}");
            // 分批次读取和插入
            $offset = 0;
            $migratedCount = 0;
            while ($offset < $totalCount) {
                // 从源表读取数据
                $selectStmt = $this->sourcePDO->prepare(
                    "SELECT * FROM {$tableName} LIMIT :limit OFFSET :offset"
                );
                $selectStmt->bindValue(':limit', $this->batchSize, PDO::PARAM_INT);
                $selectStmt->bindValue(':offset', $offset, PDO::PARAM_INT);
                $selectStmt->execute();
                $rows = $selectStmt->fetchAll();
                if (empty($rows)) {
                    break;
                }
                // 如果提供了转换函数,处理数据
                if ($transform) {
                    $rows = array_map($transform, $rows);
                }
                // 批量插入到目标表
                $this->batchInsert($tableName, $rows);
                $migratedCount += count($rows);
                $offset += $this->batchSize;
                $this->log("进度: {$migratedCount}/{$totalCount}");
            }
            $this->log("表 {$tableName} 迁移完成,共迁移 {$migratedCount} 条记录");
            return $migratedCount;
        } catch (Exception $e) {
            $this->log("迁移表 {$tableName} 失败: " . $e->getMessage(), 'ERROR');
            throw $e;
        }
    }
    // 批量插入数据
    private function batchInsert($tableName, $rows) {
        if (empty($rows)) {
            return;
        }
        $columns = array_keys($rows[0]);
        $columnList = implode(', ', $columns);
        $placeholders = '(' . implode(', ', array_fill(0, count($columns), '?')) . ')';
        $sql = "INSERT INTO {$tableName} ({$columnList}) VALUES " . 
               implode(', ', array_fill(0, count($rows), $placeholders));
        try {
            $this->targetPDO->beginTransaction();
            $stmt = $this->targetPDO->prepare($sql);
            $parameters = [];
            foreach ($rows as $row) {
                foreach ($columns as $column) {
                    $parameters[] = $row[$column];
                }
            }
            $stmt->execute($parameters);
            $this->targetPDO->commit();
        } catch (Exception $e) {
            $this->targetPDO->rollBack();
            $this->log("批量插入失败: " . $e->getMessage(), 'ERROR');
            throw $e;
        }
    }
    // 复杂迁移示例:关联表数据迁移
    public function migrateComplexData() {
        $this->log("开始复杂数据迁移");
        try {
            // 从源表读取
            $sql = "
                SELECT 
                    o.id as old_order_id,
                    o.order_no,
                    o.customer_id,
                    o.amount,
                    o.created_at,
                    c.name as customer_name,
                    c.email as customer_email
                FROM old_orders o
                LEFT JOIN old_customers c ON o.customer_id = c.id
                WHERE o.created_at < '2023-01-01'
                ORDER BY o.created_at ASC
            ";
            $stmt = $this->sourcePDO->query($sql);
            // 目标表插入
            $insertSql = "
                INSERT INTO new_orders (
                    order_no, customer_id, amount, 
                    customer_name, customer_email, created_at, 
                    migrated_at, migration_batch
                ) VALUES (
                    :order_no, :customer_id, :amount,
                    :customer_name, :customer_email, :created_at,
                    NOW(), :batch
                )
            ";
            $insertStmt = $this->targetPDO->prepare($insertSql);
            $this->targetPDO->beginTransaction();
            $batch = date('YmdHis');
            $count = 0;
            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                // 数据转换处理
                $row['amount'] = (float)$row['amount'];
                $row['created_at'] = date('Y-m-d H:i:s', strtotime($row['created_at']));
                $insertStmt->execute([
                    ':order_no' => $row['order_no'],
                    ':customer_id' => $row['customer_id'],
                    ':amount' => $row['amount'],
                    ':customer_name' => $row['customer_name'],
                    ':customer_email' => $row['customer_email'],
                    ':created_at' => $row['created_at'],
                    ':batch' => $batch
                ]);
                $count++;
                // 每1000条提交一次
                if ($count % 1000 == 0) {
                    $this->targetPDO->commit();
                    $this->targetPDO->beginTransaction();
                    $this->log("已迁移 {$count} 条复杂数据");
                    // 源表标记已迁移
                    $this->markAsMigrated($row['old_order_id']);
                }
            }
            $this->targetPDO->commit();
            $this->log("复杂数据迁移完成,共迁移 {$count} 条记录");
        } catch (Exception $e) {
            if ($this->targetPDO->inTransaction()) {
                $this->targetPDO->rollBack();
            }
            $this->log("复杂数据迁移失败: " . $e->getMessage(), 'ERROR');
            throw $e;
        }
    }
    // 标记数据已迁移
    private function markAsMigrated($id) {
        try {
            $stmt = $this->sourcePDO->prepare(
                "UPDATE old_orders SET migrated = 1 WHERE id = ?"
            );
            $stmt->execute([$id]);
        } catch (Exception $e) {
            $this->log("标记迁移状态失败: " . $e->getMessage(), 'WARNING');
        }
    }
    // 数据完整性验证
    public function validateMigration($tableName) {
        $this->log("开始验证表 {$tableName} 的数据完整性");
        try {
            // 对比记录数
            $sourceCount = $this->sourcePDO->query("SELECT COUNT(*) FROM {$tableName}")->fetchColumn();
            $targetCount = $this->targetPDO->query("SELECT COUNT(*) FROM {$tableName}")->fetchColumn();
            $match = ($sourceCount == $targetCount);
            $this->log("记录数对比 - 源表: {$sourceCount}, 目标表: {$targetCount}, 匹配: " . ($match ? '是' : '否'));
            // 如果数量不匹配,进行详细检查
            if (!$match) {
                $sampleData = $this->sampleCheck($tableName);
                return $sampleData;
            }
            return ['source_count' => $sourceCount, 'target_count' => $targetCount, 'match' => true];
        } catch (Exception $e) {
            $this->log("验证失败: " . $e->getMessage(), 'ERROR');
            return false;
        }
    }
    // 抽样检查
    private function sampleCheck($tableName, $sampleSize = 100) {
        $this->log("进行抽样检查...");
        // 从源表取样本
        $sourceSample = $this->sourcePDO->query(
            "SELECT * FROM {$tableName} ORDER BY RAND() LIMIT {$sampleSize}"
        )->fetchAll();
        $mismatches = [];
        foreach ($sourceSample as $row) {
            $id = $row['id'];
            $targetCheck = $this->targetPDO->prepare(
                "SELECT * FROM {$tableName} WHERE id = ?"
            );
            $targetCheck->execute([$id]);
            $targetRow = $targetCheck->fetch();
            if ($targetRow != $row) {
                $mismatches[] = [
                    'id' => $id,
                    'source' => $row,
                    'target' => $targetRow
                ];
            }
        }
        $this->log("抽样检查完成,发现 " . count($mismatches) . " 个不匹配项");
        return $mismatches;
    }
}

独立的迁移执行脚本

<?php
/**
 * 独立的迁移执行脚本
 */
require_once 'DatabaseMigrator.php';
// 配置
$sourceConfig = [
    'host' => 'old-db-host',
    'database' => 'old_database',
    'username' => 'old_user',
    'password' => 'old_password'
];
$targetConfig = [
    'host' => 'new-db-host',
    'database' => 'new_database',
    'username' => 'new_user',
    'password' => 'new_password'
];
// 创建迁移实例
$migrator = new DatabaseMigrator($sourceConfig, $targetConfig);
// 执行迁移
try {
    echo "=== 开始数据库迁移 ===\n\n";
    // 1. 基本表迁移
    echo "1. 迁移用户表...\n";
    $userCount = $migrator->migrateTable('users');
    echo "用户表迁移完成: {$userCount} 条记录\n\n";
    echo "2. 迁移订单表...\n";
    $orderCount = $migrator->migrateTable('orders', function($row) {
        // 数据转换
        if (isset($row['status'])) {
            $statusMap = [
                '1' => 'pending',
                '2' => 'processing',
                '3' => 'completed',
                '4' => 'cancelled'
            ];
            $row['status'] = isset($statusMap[$row['status']]) ? 
                            $statusMap[$row['status']] : 'unknown';
        }
        return $row;
    });
    echo "订单表迁移完成: {$orderCount} 条记录\n\n";
    // 2. 复杂数据迁移
    echo "3. 执行复杂数据迁移...\n";
    $migrator->migrateComplexData();
    echo "复杂数据迁移完成\n\n";
    // 3. 数据验证
    echo "4. 验证数据完整性...\n";
    $validationResult = $migrator->validateMigration('users');
    print_r($validationResult);
    echo "\n=== 数据库迁移完成 ===\n";
} catch (Exception $e) {
    echo "迁移失败: " . $e->getMessage() . "\n";
    exit(1);
}

带恢复机制的迁移脚本

<?php
/**
 * 带检查点和恢复机制的迁移脚本
 */
class AdvancedMigrator extends DatabaseMigrator {
    private $checkpointFile;
    private $checkpoint;
    public function __construct($sourceConfig, $targetConfig, $checkpointFile = null) {
        parent::__construct($sourceConfig, $targetConfig);
        $this->checkpointFile = $checkpointFile ?? __DIR__ . '/migration_checkpoint.json';
        $this->loadCheckpoint();
    }
    // 加载检查点
    private function loadCheckpoint() {
        if (file_exists($this->checkpointFile)) {
            $json = file_get_contents($this->checkpointFile);
            $this->checkpoint = json_decode($json, true);
        } else {
            $this->checkpoint = [];
        }
    }
    // 保存检查点
    private function saveCheckpoint() {
        file_put_contents($this->checkpointFile, json_encode($this->checkpoint, JSON_PRETTY_PRINT));
    }
    // 记录迁移进度
    private function recordProgress($task, $completed, $total, $extra = []) {
        $this->checkpoint[$task] = [
            'completed' => $completed,
            'total' => $total,
            'timestamp' => date('Y-m-d H:i:s'),
            'extra' => $extra
        ];
        $this->saveCheckpoint();
    }
    // 执行可恢复的迁移
    public function migrateWithResume($tableName) {
        $taskKey = "table_{$tableName}";
        // 如果已有检查点,从上次位置继续
        if (isset($this->checkpoint[$taskKey])) {
            $lastPosition = $this->checkpoint[$taskKey]['completed'];
            $this->log("从上次位置恢复: {$lastPosition}");
            return $this->resumeTableMigration($tableName, $lastPosition);
        }
        // 全新迁移
        try {
            $this->log("开始迁移表: {$tableName}");
            $countQuery = "SELECT COUNT(*) FROM {$tableName}";
            $totalCount = $this->sourcePDO->query($countQuery)->fetchColumn();
            // 如果是第一次,记录总记录数
            if (!isset($this->checkpoint[$taskKey])) {
                $this->recordProgress($taskKey, 0, $totalCount);
            }
            $offset = 0;
            $transferred = 0;
            while ($transferred < $totalCount) {
                // 检查是否需要恢复
                if (isset($this->checkpoint[$taskKey]['completed'])) {
                    $offset = $this->checkpoint[$taskKey]['completed'];
                    unset($this->checkpoint[$taskKey]['completed']); // 清除恢复标记
                }
                // 读取和处理数据
                $selectStmt = $this->sourcePDO->prepare(
                    "SELECT * FROM {$tableName} LIMIT :limit OFFSET :offset"
                );
                $selectStmt->bindValue(':limit', $this->batchSize, PDO::PARAM_INT);
                $selectStmt->bindValue(':offset', $offset, PDO::PARAM_INT);
                $selectStmt->execute();
                $rows = $selectStmt->fetchAll();
                if (empty($rows)) break;
                // 批量插入
                $this->batchInsert($tableName, $rows);
                $offset += count($rows);
                $transferred += count($rows);
                // 更新进度
                $this->recordProgress($taskKey, $offset, $totalCount);
                $this->log("进度: {$offset}/{$totalCount}");
            }
            $this->log("表 {$tableName} 迁移完成");
            return true;
        } catch (Exception $e) {
            $this->log("迁移中断: " . $e->getMessage(), 'ERROR');
            $this->log("可以在下次运行时自动从断点恢复", 'INFO');
            throw $e;
        }
    }
    // 断点续传函数
    private function resumeTableMigration($tableName, $position) {
        return $this->migrateWithResume($tableName);
    }
}
// 使用示例
$config = [
    'source' => [
        'host' => 'localhost',
        'database' => 'old_db',
        'username' => 'user',
        'password' => 'pass'
    ],
    'target' => [
        'host' => 'localhost',
        'database' => 'new_db',
        'username' => 'user',
        'password' => 'pass'
    ]
];
$migrator = new AdvancedMigrator($config['source'], $config['target']);
$migrator->migrateWithResume('users');

命令行迁移工具

<?php
/**
 * 命令行迁移脚本工具
 * 使用方法: php migrate.php --table=table_name [options]
 */
require_once 'DatabaseMigrator.php';
// 命令行参数解析
$options = getopt('', ['table:', 'source:', 'target:', 'help']);
function showHelp() {
    echo <<<HELP
PHP数据迁移工具
用法: php migrate.php [options]
选项:
  --table=TABLE      指定要迁移的表名
  --source=CONFIG   源数据库配置(JSON格式)
  --target=CONFIG   目标数据库配置(JSON格式)
  --help            显示帮助信息
示例:
  php migrate.php --table=orders
  php migrate.php --table=users --source='{"host":"localhost","database":"db1"}'
HELP;
}
if (isset($options['help'])) {
    showHelp();
    exit(0);
}
// 加载配置
$configFile = __DIR__ . '/config.php';
if (file_exists($configFile)) {
    $config = require $configFile;
} else {
    $config = [
        'source' => json_decode($options['source'] ?? '{}', true),
        'target' => json_decode($options['target'] ?? '{}', true)
    ];
}
try {
    $migrator = new DatabaseMigrator($config['source'], $config['target']);
    if (isset($options['table'])) {
        // 迁移单个表
        $result = $migrator->migrateTable($options['table']);
        echo "迁移完成: {$result} 条记录\n";
    } else {
        // 迁移所有表
        $tables = ['users', 'orders', 'products', 'categories'];
        foreach ($tables as $table) {
            $migrator->migrateTable($table);
        }
    }
} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
    exit(1);
}

配置文件示例

<?php
// config.php - 迁移配置
return [
    'source' => [
        'host' => env('OLD_DB_HOST', 'localhost'),
        'database' => env('OLD_DB_DATABASE', 'old_database'),
        'username' => env('OLD_DB_USERNAME', 'old_user'),
        'password' => env('OLD_DB_PASSWORD', 'old_password'),
    ],
    'target' => [
        'host' => env('NEW_DB_HOST', 'localhost'),
        'database' => env('NEW_DB_DATABASE', 'new_database'),
        'username' => env('NEW_DB_USERNAME', 'new_user'),
        'password' => env('NEW_DB_PASSWORD', 'new_password'),
    ],
    'batch_size' => 1000,
    'tables' => [
        'users' => [
            'transform' => function($row) {
                // 添加迁移标记
                $row['migrated_at'] = date('Y-m-d H:i:s');
                $row['migration_batch'] = getenv('MIGRATION_BATCH');
                return $row;
            }
        ],
        'orders' => [
            'conditions' => "WHERE created_at >= '2023-01-01'",
            'transform' => function($row) {
                // 状态转换
                $statusMap = ['pending', 'processing', 'completed'];
                $row['status'] = isset($statusMap[$row['status_id']]) ? 
                                $statusMap[$row['status_id']] : 'unknown';
                return $row;
            }
        ]
    ]
];

使用环境变量

// .env 文件示例
OLD_DB_HOST=localhost
OLD_DB_DATABASE=old_database
OLD_DB_USERNAME=root
OLD_DB_PASSWORD=secret
NEW_DB_HOST=localhost
NEW_DB_DATABASE=new_database
NEW_DB_USERNAME=root
NEW_DB_PASSWORD=newsecret
MIGRATION_BATCH=20240101

运行方式

# 运行迁移
php migrate.php --table=users
# 带自定义配置
php migrate.php --table=orders --source='{"host":"192.168.1.100","database":"old_db"}'
# 执行完整迁移
php migrate.php --all
# 带调试输出
php -d display_errors=1 migrate.php --table=users

注意事项

  1. 备份: 迁移前务必备份源数据库
  2. 测试: 建议先在测试环境验证迁移脚本
  3. 性能: 设置合适的批次大小,避免内存溢出
  4. 错误处理: 迁移脚本应该有完善的错误处理和日志记录
  5. 断点续传: 对大表启用断点续传功能
  6. 数据验证: 迁移后执行数据完整性检查

这个脚本提供了完整的数据库迁移解决方案,可根据实际需求进行调整和扩展。

抱歉,评论功能暂时关闭!