插件静默安装后数据库表"人间蒸发":我追踪到 `dbDelta` 与字符集声明之间那条"隐形裂缝"

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
插件开发 91 浏览 0 回复

上周帮客户迁移环境,插件安装流程跑完没报错,后台菜单正常、设置页能进,唯独核心功能一片空白。查数据库,自定义表压根没创建。更诡异的是本地复现不了,线上必现。

先贴当时的"自信代码":

function myplugin_activate() {
    global $wpdb;
    $charset_collate = $wpdb->get_charset_collate();
    
    $sql = "CREATE TABLE {$wpdb->prefix}myplugin_logs (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        action varchar(50) NOT NULL,
        created_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
        PRIMARY KEY (id)
    ) $charset_collate;";
    
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
}

问题藏在两个地方,单独看都对,合起来就裂。

第一坑:`dbDelta` 的换行洁癖

我为了可读性把 SQL 拆成多行,每行开头留了空格缩进。dbDelta 内部用正则匹配字段定义,要求字段行必须以字段名开头,前面不能有空格。我的缩进被它当成"这不是字段行"直接跳过,整段 SQL 解析后只剩表头,自然建不出表。

修正后必须长这样,字段行顶格:

$sql = "CREATE TABLE {$wpdb->prefix}myplugin_logs (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
action varchar(50) NOT NULL,
created_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";

第二坑:字符集声明的"半条命"

本地 MySQL 8.0,线上 5.7。$charset_collate 在 8.0 返回 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci,5.7 没有 unicode_520_ci,只有 unicode_ci。但这不是报错原因——真正的问题是 dbDelta 会把现有表结构和传入 SQL 做字符串比对,如果字符集声明写法不一致(比如多了个空格、少了 DEFAULT 关键字),它会认为"表结构有变更"尝试 ALTER,而 ALTER 失败时它选择静默吞掉。

更隐蔽的是:如果表已经存在但结构"看起来不同",dbDelta 返回空数组,既不报错也不重建。我迁移前手动删了表数据但表结构残留,dbDelta 比对后觉得"差不多",直接放行。

我的排查组合拳

1. 先给 dbDelta 套个返回值监控:

$result = dbDelta($sql);
error_log('dbDelta result: ' . print_r($result, true));

返回空数组不代表成功,只代表"我没干活"。

2. 激活钩子末尾强制验表:

if ($wpdb->get_var("SHOW TABLES LIKE '{$wpdb->prefix}myplugin_logs'") !== $wpdb->prefix . 'myplugin_logs') {
    set_transient('myplugin_activate_error', 'Table missing: ' . $wpdb->last_error, 60);
}

3. 用 SHOW CREATE TABLE 把线上实际结构和本地比对,抓出字符集差异。

现在的防御式写法

function myplugin_activate() {
    global $wpdb;
    
    // 先干净利落地删掉残留结构,避免 dbDelta 的"差不多"逻辑
    $table_name = $wpdb->prefix . 'myplugin_logs';
    $wpdb->query("DROP TABLE IF EXISTS $table_name");
    
    $charset_collate = $wpdb->get_charset_collate();
    
    // 字段行必须顶格,KEY 行前面要有空格——这是 dbDelta 唯一允许的缩进
    $sql = "CREATE TABLE $table_name (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
action varchar(50) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (id),
KEY created_at (created_at)
) $charset_collate;";
    
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
    
    // 二次确认,失败即抛异常阻断激活
    if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
        wp_die('Database table creation failed. Check error log for details.');
    }
}

另外把 DEFAULT '0000-00-00 00:00:00' 改成了 CURRENT_TIMESTAMP,MySQL 5.7 在严格模式下会拒掉零日期,这又是另一个线上特供坑。

最后提一嘴:如果插件已经发出去、用户那可能有残留表结构,DROP TABLE IF EXISTS 会丢数据。更稳妥的做法是版本化迁移脚本,用 dbDelta 的"比对 ALTER"能力,但前提是 SQL 写法必须和它胃口完全一致。我现在的方案是首次安装走 DROP + CREATE,升级走 dbDelta 比对,中间用 option 标记版本号隔离。

有人踩过 dbDelta 对索引名大小写的诡异处理吗?本地 Windows 不区分,线上 Linux 区分,导致重复索引报错。这货简直是环境差异的放大器。

评论0
回复 · 0
还没有回复
微信客服 微信客服