数据库迁移脚本里的"幽灵表"与"僵尸字段":我在 `dbDelta` 升级流程里被 `UNIQUE KEY` 长度和 `COLLATE` 差异坑了四次的血泪史

插件开发 17 浏览 0 回复 返回上级

去年给公司会员系统写迁移脚本,自以为 `dbDelta` 用熟了,结果在 staging 环境反复翻车。最恶心的一次:本地升级完美,客户服务器上直接报 "Duplicate entry" 然后整个插件激活流程中断,数据半吊子挂着。后来才摸清 `dbDelta` 那套"看起来帮你、实则坑你"的隐式规则。

先说第一个坑:UNIQUE KEY 的字段顺序与长度限制。我建了张积分流水表,想当然写了:

$sql = "CREATE TABLE {$wpdb->prefix}my_credits (
    id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    user_id bigint(20) unsigned NOT NULL,
    action_type varchar(50) NOT NULL,
    related_id bigint(20) unsigned NOT NULL,
    created_at datetime DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY user_action (user_id, action_type, related_id)
) {$wpdb->get_charset_collate()};";

本地 MySQL 8.0 没事,客户是 5.7 + utf8mb4,UNIQUE KEY 三字段加起来超了 3072 字节限制,dbDelta 执行时没报错,但 SHOW CREATE TABLE 一看 key 根本没建起来。更阴的是 dbDelta 的返回数组里这个属于 "query" 成功,不会进 errors。我现在的做法是升级前手动算索引长度,超长的拆成前缀索引或者额外哈希字段。

第二个坑更隐蔽:COLLATE 不一致导致的"表存在但字段不匹配"。插件 1.0 建表时没显式指定 COLLATE,跟着服务器默认走了 utf8mb4_general_ci;1.2 升级时我手写 SQL 补字段,顺手带了 utf8mb4_unicode_cidbDelta 比较字段定义时,COLLATE 差异会触发 ALTER TABLE,但如果表里已有百万级数据,某些 MySQL 版本会锁表到怀疑人生。我现在强制所有建表语句末尾统一用 {$wpdb->get_charset_collate()},升级补丁里绝不再硬编码字符集。

第三个坑是卸载时的"外键幽灵"。有张订单表引了用户表的 user_id,我在 register_uninstall_hook 里按顺序 DROP TABLE,结果外键约束让第二个 DROP 直接挂掉,整个卸载流程中断,插件文件删了但表还在,重新安装时 dbDelta 看到表存在就跳过建表,然后代码里查新字段直接 Undefined index。现在的卸载脚本我先查 INFORMATION_SCHEMA.KEY_COLUMN_USAGE 把外键全删了再动表,顺序是:外键 → 索引 → 表。

第四个坑最反直觉:dbDeltaDEFAULT 值的处理。1.3 版本我给 status 字段从 DEFAULT 'pending' 改成 DEFAULT 'active'dbDelta 对比时认为这属于字段定义变更,会生成 ALTER TABLE ... MODIFY COLUMN。但如果原表已有数据,MySQL 5.7 里这个 MODIFY 会重写整列,大表直接爆炸。我的 workaround 是:默认值变更不走 dbDelta,单独写 UPDATE 设新默认值,然后 ALTER TABLE ... ALTER COLUMN ... SET DEFAULT 只改元数据。

现在我的迁移脚本结构长这样,供参考:

class My_Plugin_Migrator {
    private static $schema_version = '1.4.2';
    
    public static function install() {
        global $wpdb;
        $installed = get_option('my_plugin_db_version', '0.0.0');
        
        if (version_compare($installed, self::$schema_version, '>=')) {
            return;
        }
        
        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
        
        // 先建/更核心表
        self::create_core_tables();
        
        // 再跑数据迁移(按版本阶梯)
        self::run_data_migrations($installed);
        
        // 最后补索引/约束
        self::ensure_indexes();
        
        update_option('my_plugin_db_version', self::$schema_version);
    }
    
    private static function create_core_tables() {
        global $wpdb;
        // 所有 COLLATE 强制统一,UNIQUE KEY 长度预检
        // ...
    }
    
    private static function run_data_migrations($from_version) {
        $migrations = [
            '1.1.0' => 'migrate_110_split_meta',
            '1.2.0' => 'migrate_120_status_enum',
            '1.3.0' => 'migrate_130_add_hash_index',
            '1.4.0' => 'migrate_140_archive_old_logs',
        ];
        
        foreach ($migrations as $ver => $method) {
            if (version_compare($from_version, $ver, '
评论0
回复 · 0
还没有回复
微信客服 微信客服