3.3 Table 类封装与数据库 API
数据操作别再手拼 SQL 字符串,把表名交给系统去拼。
为什么内核要用 Table 类
从 Discuz! X2.5 起,内核把数据表做了封装,封装后的文件统一命名为 Table 类,通过 C::t('Table类文件名') 调用。
它解决的是一个很具体的麻烦:表前缀。Discuz! 允许安装时自定义表前缀,官方文档里写 cdb_,实际可能是 discuz_、也可能被站长改成别的。如果你在代码里写死 discuz_common_member,换一个前缀的站点就全线报错。Table 类把「拼前缀」这步收进内核,业务代码只写 C::t('common_member')。
对比一下这两种写法:
| 写法 | 表前缀变了会怎样 |
|---|---|
DB::query('SELECT * FROM cdb_member WHERE uid=1') |
立刻报错 |
DB::query('SELECT * FROM '.DB::table('common_member').' WHERE uid=1') |
正常 |
C::t('common_member')->find_first(array('uid' => 1)) |
正常 |
插件的 Table 类放哪、怎么命名
插件要封装自己的表,路径是固定的:source/plugin/{标识}/table/,文件名 table_{表名}.php。对应关系看下面这个例子——文件 source/plugin/myplugin/table/table_myplugin_log.php 对应数据表 myplugin_log:
| 调用 | 对应文件 |
|---|---|
C::t('myplugin_log') |
table/table_myplugin_log.php |
C::t('myplugin_log')->insert($data) |
同上 |
C::t('plugin_sample') |
内核的系统表(插件注册表) |
C::t() 传的是「Table 类文件名」,不是完整表名,也不是带前缀的表名。 系统会先在 source/class/db/table/ 里找,找不到再去 source/plugin/{标识}/table/ 里找。这个查找顺序就是「插件类和系统表同名时以系统表为准」的原因——所以插件表名带上自己的标识前缀,是防冲突最省事的办法。
Table 类骨架
下面这份骨架可以直接复制。基类为 discuz_table(具体基类名和可用方法以你版本里 source/class/db/ 下的实际定义为准):
<?php
if(!defined('IN_DISCUZ')) {
exit('Access Denied');
}
class table_myplugin_log extends discuz_table {
// 表名,不含前缀
protected $tablename = 'myplugin_log';
// 主键字段名,单主键时用字符串,多主键时用数组
protected $pk = 'logid';
// 需要初始化的字段,用于 insert/update 时过滤非法字段
public $pkname = 'logid';
public $staticvars = array();
public function __construct() {
$this->_init(
$this->tablename,
$this->pk,
$this->pkname
);
}
public function delete_by_uid($uid) {
if(!$uid) {
return false;
}
return $this->delete(array('uid' => $uid));
}
}
这个文件放在 source/plugin/myplugin/table/table_myplugin_log.php。构造函数里的 _init() 把表名和主键交给基类,这是继承 Table 类时最不能省的一步——省了它,insert() 不知道往哪张表写。
常用方法与调用示例
拿到实例之后是链式操作,下面这些是最常用的几个:
global $_G, $discuz;
// 1. 插入:返回受影响行数;需要自增ID时传第二个参数
$logid = C::t('myplugin_log')->insert(array(
'uid' => $_G['uid'],
'action' => 'checkin',
'note' => '每日签到',
'dateline' => TIMESTAMP,
), true);
// 2. 改:条件在前,数据在后,返回受影响行数
C::t('myplugin_log')->update(array('logid' => $logid), array(
'note' => '已补发积分',
));
// 3. 删:传条件,返回受影响行数
C::t('myplugin_log')->delete(array('logid' => $logid));
// 4. 取一条:返回单行数组或 false
$row = C::t('myplugin_log')->find_first(array('uid' => $_G['uid']));
// 5. 取一批:fetch_all 接收 SQL 与参数,返回结果集
$rows = C::t('myplugin_log')->fetch_all(
'SELECT * FROM '.DB::table('myplugin_log').' WHERE uid=? ORDER BY logid DESC LIMIT 20',
array($_G['uid'])
);
// 6. 计数
$total = C::t('myplugin_log')->count(array('uid' => $_G['uid']));
注意第 5 条:fetch_all() 里的 SQL 仍然需要 DB::table() 来取全名,因为这是原始 SQL 通道,Table 类只替你处理了封装方法的表名。select()、find_first()、update()、delete() 这类封装方法内部自动处理表名,不用你管。
几个方法名在不同版本里可能略有差异(fetch_all / fetchfield / find 之类),以你版本里基类的实际定义为准,source/class/db/ 下搜一下基类就清楚了。
表名全名的取得:DB::table()
DB::table('tablename') 返回带前缀的完整表名,比如 discuz_common_member。它是拼接 SQL 时的唯一正确姿势:
// 正确:前缀由系统拼
$sql = 'SELECT COUNT(*) FROM '.DB::table('myplugin_log').' WHERE dateline < '.intval($_GET['t']);
// 错误:写死前缀,换站点就炸
$sql = 'SELECT COUNT(*) FROM cdb_myplugin_log WHERE dateline < '.intval($_GET['t']);
永远不要在业务代码里写死表前缀。 这条规矩在安装脚本里是个例外——那里 runquery() 的 cdb_ 前缀是官方特意的约定。但只要进了业务代码,就得老老实实用 DB::table() 或 Table 类。
顺带一个数据规范上的建议:把插件用到的数据表名称(不含前缀)在后台插件管理里准确设置好,这样站点做数据备份时会带上你的表。
直接操作数据库的情况
Table 类不是唯一入口。复杂聚合、多表 JOIN、批量原生语句,走 DB:: 更直接:
| 场景 | 建议 |
|---|---|
| 单表的增删改查 | C::t() |
| 多表 JOIN、复杂聚合 | DB::query() 配 DB::table() |
| 取完整表名拼 SQL | DB::table('tablename') |
| 事务批量操作 | 走 DB 的事务方法 |
原则是:单表用封装,多表用原生。硬把 JOIN 塞进 fetch_all() 之外的封装方法里,代码会迅速变得难读。
与 3.2 的衔接
Table 类解决的是运行时的表名前缀问题,安装脚本解决的是结构从无到有的问题,两者是接上的:你在 install.php 里用 runquery() 建了 cdb_myplugin_log,运行时用 C::t('myplugin_log') 操作同一张表——Table 类的 protected $tablename 正好填 3.2 里不带前缀的那个名字。建表名和 Table 类名对不上,是新手最常见的「插件装了但读不出数据」原因。
