EP164. “用 dbDelta 建表:activate_ 钩子与 $wpdb”
🔒 登录后可标记已读开始写自定义数据库表版本的宠物领养插件(跟 EP163 的 custom post type 版本功能一样,但换成自己建表)。用课程提供的 Starter 模板跳过跟数据库无关的样板代码,核心内容是:用 WordPress 专属的 activate_{插件文件路径} 钩子,只在插件被激活的那一刻执行「建表」逻辑;用官方推荐但写法有点冷门的 dbDelta() 函数根据一段 SQL 建表语句创建/更新表结构(这个函数很挑剔 SQL 语句的格式,写错格式很容易静默失败);表名前缀和字符集不能写死,要从全局 $wpdb 对象里动态取,因为不同 WordPress 安装的表前缀可能不是默认的 wp_。最后用 $wpdb->insert() 往新表里插入一条测试数据,验证整条链路。
涉及文件
wp-content/plugins/new-database-table/new-database-table.php(修改,基于课程提供的 Starter 模板)
代码实现
new-database-table.php(完整文件):
<?php
/*
Plugin Name: Pet Adoption (New DB Table)
Version: 1.0
Author: Brad
Author URI: https://www.udemy.com/user/bradschiff/
*/
if( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
require_once plugin_dir_path(__FILE__) . 'inc/generatePet.php';
class PetAdoptionTablePlugin {
function __construct() {
global $wpdb;
$this->charset = $wpdb->get_charset_collate();
$this->tablename = $wpdb->prefix . "pets";
add_action('activate_new-database-table/new-database-table.php', array($this, 'onActivate'));
add_action('admin_head', array($this, 'onAdminRefresh'));
add_action('wp_enqueue_scripts', array($this, 'loadAssets'));
add_filter('template_include', array($this, 'loadTemplate'), 99);
}
function onActivate() {
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta("CREATE TABLE $this->tablename (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
birthyear smallint(5) NOT NULL DEFAULT 0,
petweight smallint(5) NOT NULL DEFAULT 0,
favfood varchar(60) NOT NULL DEFAULT '',
favhobby varchar(60) NOT NULL DEFAULT '',
favcolor varchar(60) NOT NULL DEFAULT '',
petname varchar(60) NOT NULL DEFAULT '',
species varchar(60) NOT NULL DEFAULT '',
PRIMARY KEY (id)
) $this->charset;");
}
function onAdminRefresh() {
global $wpdb;
$wpdb->insert($this->tablename, generatePet());
}
function loadAssets() {
if (is_page('pet-adoption')) {
wp_enqueue_style('petadoptioncss', plugin_dir_url(__FILE__) . 'pet-adoption.css');
}
}
function loadTemplate($template) {
if (is_page('pet-adoption')) {
return plugin_dir_path(__FILE__) . 'inc/template-pets.php';
}
return $template;
}
}
$petAdoptionTablePlugin = new PetAdoptionTablePlugin();
关键改动点:
- Starter 模板:
loadAssets()(加载 CSS)和loadTemplate()(把/pet-adoption这个页面的模板替换成插件自带的inc/template-pets.php)已经写好,onActivate()和onAdminRefresh()一开始是空函数体,这一讲的任务就是把这两个函数填上 activate_{插件文件夹名}/{插件主文件名}:这是 WordPress 专属的一类动态命名 hook,只在这个插件被激活的那一刻触发一次(停用后重新激活会再触发一次,但平时插件保持激活状态时不会反复运行)——非常适合放「建表」这种只需要设置一次结构、不需要每次请求都重新执行的初始化逻辑。这个 hook 名字必须跟插件实际的文件夹名/主文件名完全匹配,这里是字面量字符串'activate_new-database-table/new-database-table.php',不是拼出来的动态字符串require_once(ABSPATH . 'wp-admin/includes/upgrade.php'):dbDelta()函数默认不在全局可用,必须先手动引入 WordPress 自带的这个系统文件才能使用——作者坦言自己第一次看到这个做法时觉得像是「非官方的奇怪手法」,但这正是 WordPress 官方文档演示建表时推荐的标准方式dbDelta($SQL建表语句):这个函数的聪明之处在于「幂等」——如果表已经存在,它会比较现有表结构和传入的 SQL 语句之间的差异(delta就是「差异」的意思),只做必要的调整,不会把整张表删掉重建、不会丢失已有数据;即使插件被停用又重新激活很多次,也不会重复破坏数据dbDelta()对 SQL 语句格式极其挑剔,必须严格遵守官方文档列出的一套格式规则,否则会静默失败且不容易排查,这一讲踩到/提到的具体规则:- 每一个字段定义必须独立成一行
PRIMARY KEY和后面的括号定义之间必须有两个空格(不是一个,也不是零个)- 必须用
KEY这个词,不能用它的同义词INDEX - 建表语句里不能有多余的反引号等其他微妙的格式差异
- 表名前缀不能硬编码成
wp_:不同的 WordPress 安装在初始设置时可以自定义数据库表前缀(不一定是默认的wp_),所以要通过全局对象$wpdb动态获取:$wpdb->prefix . "pets"拼出完整表名(比如wp_pets),存到$this->tablename属性上,方便类里所有方法共用 - 字符集/校对规则同样要动态获取:
$wpdb->get_charset_collate()返回当前数据库配置的字符集和排序规则字符串,拼接到建表 SQL 语句末尾($this->charset),而不是自己猜一个写死 - 在构造函数里统一读取并存成属性(
$this->charset/$this->tablename),而不是每个用到的方法各自单独去读一次全局$wpdb——这是因为预期后面不止一个方法会用到这两个动态值,统一在构造函数里存一次更方便复用 - 列类型设计:
id用bigint(20) unsigned NOT NULL AUTO_INCREMENT自动生成主键,不需要手动指定 ID;birthyear/petweight用smallint(5),数值类字段给了DEFAULT 0;favfood/favhobby/favcolor/petname/species用varchar(60),文本类字段给了DEFAULT ''(空字符串)——所有字段都加了NOT NULL,配合默认值保证不会出现「空值」这种额外要处理的边界情况 PRIMARY KEY (id):把id这一列设为主键(数据库自动为主键建立索引,方便快速定位/关联)- 测试插入数据:
onAdminRefresh()绑定在admin_head这个 hook 上(每次刷新后台页面都会执行),内部调用$wpdb->insert($this->tablename, generatePet())——$wpdb->insert()的第一个参数是表名,第二个参数是一个「列名 => 值」的关联数组,generatePet()(Starter 模板自带的辅助函数,放在inc/generatePet.php)负责随机生成一只假宠物的完整数据。这一步只是为了验证「表建好了、真的能写入数据」,之后会替换成真正的后台表单提交 - course 也提供了一个
populateFast()方法(把admin_head改绑到这个方法上就能用),内部直接拼一整条批量INSERT INTO ... VALUES (...), (...), ...的原生 SQL 语句、透过$wpdb->query()执行,一次性插入 10 万条测试数据——课程作者特别在代码注释里强调:这里能直接拼接原始 SQL 字符串,是因为数据来自受信任的自己写的假数据生成函数,真实场景下绝对不能这样拼接用户输入,必须配合$wpdb->prepare()做参数转义,这里只是为了这个演示脚本执行更快、占用内存更少的权宜之计
[截图:phpMyAdmin/Adminer 里新建成功的 wp_pets 表结构(id/birthyear/petweight/favfood 等各列)和插入的第一条测试数据]
Hook / Function 速查
| 名称 | 类型 | 用途 |
|---|---|---|
activate_{插件文件夹}/{插件主文件} | WP 动态 hook | 插件被激活的那一刻触发一次,适合放建表等一次性初始化逻辑 |
dbDelta($SQL) | WP 内建 function(需手动 require 才能用) | 根据 SQL 语句创建/智能更新表结构,不会重复执行导致丢失数据 |
$wpdb->prefix | $wpdb 全局对象属性 | 当前 WordPress 安装实际使用的数据库表前缀(不一定是 wp_) |
$wpdb->get_charset_collate() | $wpdb 方法 | 获取当前数据库配置的字符集和排序规则字符串,建表语句要用到 |
$wpdb->insert($表名, $关联数组) | $wpdb 方法 | 往指定表插入一条记录 |
$wpdb->query($原始SQL) | $wpdb 方法 | 执行任意原始 SQL 语句(不带自动转义,需要自己保证安全) |
常见坑
- 忘记先
require_once引入wp-admin/includes/upgrade.php——dbDelta()函数不存在,直接报错 - 建表 SQL 语句里字段没有各自独立成一行、或者
PRIMARY KEY后面没有精确写两个空格、或者用了INDEX代替KEY——dbDelta()对格式极其敏感,不符合规则容易静默建表失败,很难排查 - 把表前缀硬编码成
wp_——如果目标网站的实际前缀不是默认值,代码在别人的网站上会指向一张不存在的表 - 把「建表」逻辑放在
init这类每次请求都会触发的 hook 上,而不是activate_...这种只在激活时触发一次的 hook——虽然dbDelta()本身是幂等的、重复执行不会丢数据,但没必要让每次页面请求都白白执行一次不必要的建表检查 - 真实项目里直接拼接用户输入到原始 SQL 字符串再用
$wpdb->query()执行——存在 SQL 注入风险,必须配合$wpdb->prepare()做参数转义(本讲的populateFast()示例是特例,因为数据来自自己的假数据生成器,不是真实的用户输入)
延伸 / 后续讲座会用到
下一讲要学怎么写 SQL 查询语句,把前台 /pet-adoption 页面的模板从静态占位改成真正读取这张新表的数据。
Sources
Udemy:
- Become a WordPress Developer: Unlocking Power With Code — Section 27, EP164