插件静默安装后数据库表"人间蒸发":我追踪到 `dbDelta` 与字符集声明之间那条"隐形裂缝"
上周帮客户迁移环境,插件安装流程跑完没报错,后台菜单正常、设置页能进,唯独核心功能一片空白。查数据库,自定义表压根没创建。更诡异的是本地复现不了,线上必现。
先贴当时的"自信代码":
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 区分,导致重复索引报错。这货简直是环境差异的放大器。

