概述
Xiuno X 的数据库抽象层采用接口 + 驱动 + 全局函数三层架构设计。核心接口 DatabaseInterface(lib/DatabaseInterface.php)定义了统一的 CRUD 契约,驱动层 db_pdo_mysql(xiunophp/db_pdo_mysql.class.php)基于 PDO MySQL 实现主从分离与读写分离,顶层全局函数(xiunophp/db.func.php)提供便捷的过程式调用入口。
该抽象层具备以下特性:参数绑定防 SQL 注入(全部使用 ? 占位符 + PDOStatement::bindValue)、主从读写分离(自动选择 wlink/rlink)、条件数组语法(支持 >, <, >=, <=, !=, LIKE, IN 等操作符)、字段名白名单校验防止字段注入。开发者通过统一的函数调用即可完成跨数据库类型的操作,无需关心底层驱动细节。
核心接口
DatabaseInterface 方法
方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
| — |
| 建立主从连接 |
| — |
| 关闭全部连接 |
| 表名、条件、排序、分页、主键、列 |
| 多行查询(分页) |
| 表名、条件、排序、列 |
| 单行查询 |
| 含 |
| GROUP BY 聚合多行查询 |
| 同上 |
| GROUP BY 聚合单行查询 |
| 表名、数据数组 |
| 插入,返回新 ID |
| 表名、条件、数据 |
| 更新,返回影响行数 |
| 表名、条件 |
| 删除,返回影响行数 |
| 表名、条件 |
| 统计行数 |
| 表名、字段、条件 |
| 获取最大 ID |
| 表名 |
| 清空表 |
| 原始 SQL |
| 执行 DDL/DML |
| 原始 SQL |
| 原生 SQL 单条查询 |
| 原始 SQL、key 字段 |
| 原生 SQL 多条查询 |
| 带 |
| 预处理执行(自动选读写连接) |
| 同上 |
| 预处理单条查询 |
| 值 |
| 转义(底层直接用参数绑定,通常无需调用) |
| 表名(不含前缀) |
| 返回带前缀的完整表名 |
常用函数 API
全局函数通过 $_SERVER['db'] 自动获取数据库实例,可传入自定义实例 $d 作为最后一个参数。
查询函数
// 多行查询(分页 10 条,第 1 页)
db_find('user', ['status' => 1], ['uid' => -1], 1, 10);
// 单行查询
db_find_one('user', ['uid' => 1]);
// 统计行数
db_count('user', ['status' => 1]);
// 获取最大 ID
db_maxid('user', 'uid', ['status' => 1]);
// 原生 SQL 查询(复杂 JOIN/子查询场景)
db_sql_find_one('SELECT u.*, g.name FROM user u LEFT JOIN group g ON u.gid=g.id WHERE u.uid=?', [1]);写入函数
// 插入(返回新 ID)
$id = db_insert('user', ['username' => 'demo', 'email' => 'demo@test.com']);
// 替换插入(REPLACE INTO)
db_replace('user', ['uid' => 1, 'username' => 'demo']);
// 更新(支持 + / - 前缀增量操作)
db_update('user', ['uid' => 1], ['username' => 'newname', 'stocks+' => 5]);
// 删除
db_delete('user', ['uid' => 1]);
// 清空表
db_truncate('session');条件数组语法
条件数组是抽象层的核心,支持以下格式:
// 1. 简单相等
['uid' => 1, 'status' => 1]
// WHERE `uid`=? AND `status`=? params: [1, 1]
// 2. 比较操作符(后缀写法)
['uid>' => 100, 'uid<' => 200]
// WHERE `uid`>? AND `uid<? params: [100, 200]
// 3. OR 数组(IN 查询)
['uid' => [1, 2, 3]]
// WHERE (`uid`=? OR `uid`=? OR `uid`=?) params: [1, 2, 3]
// 4. 多操作符组合
['username' => ['LIKE' => 'jack'], 'status' => ['>' => 0]]
// WHERE `username` LIKE ? AND `status`>? params: ['%jack%', 0]事务支持
Xiuno X 数据库层未封装显式事务 API。需通过原生 PDO 实现事务:
$db = $_SERVER['db'];
$wlink = $db->connect_master();
try {
$wlink->beginTransaction();
$db->insert('order', ['uid' => 1, 'amount' => 100]);
$db->update('user', ['uid' => 1], ['balance-' => 100]);
$wlink->commit();
} catch (Exception $e) {
$wlink->rollBack();
}注意:事务内的写操作必须在同一连接上执行。
db_pdo_mysql的prepare()已自动将写操作路由到wlink,但db_insert/db_update等函数封装了额外逻辑,复杂事务建议直接使用$db->wlink->prepare()。
代码示例
基本查询
// 获取最新 10 条主题
$threads = db_find('thread', ['fid' => 5], ['tid' => -1], 1, 10, 'tid', ['tid', 'subject', 'uid']);
foreach ($threads as $tid => $thread) {
echo "{$thread['tid']}: {$thread['subject']}\n";
}条件查询
// 搜索主题(标题含 "PHP",属于版块 3 或 5,已审核)
$list = db_find('thread', [
'fid' => [3, 5],
'status' => 1,
'subject' => ['LIKE' => 'PHP'],
], ['tid' => -1], 1, 20);
// 获取指定用户发帖数(GROUP BY 聚合)
$stats = db_find_group('thread', ['status' => 1], ['uid'], [], ['cnt' => -1], 1, 10, 'uid', ['uid', 'COUNT(*) as cnt']);事务操作
function transfer($fromUid, $toUid, $amount) {
$db = $_SERVER['db'];
$wlink = $db->connect_master();
try {
$wlink->beginTransaction();
// 扣减转出方余额
$stmt = $wlink->prepare('UPDATE xn_user SET balance=balance-? WHERE uid=? AND balance>=?');
$stmt->execute([$amount, $fromUid, $amount]);
if ($stmt->rowCount() === 0) {
throw new Exception('余额不足');
}
// 增加转入方余额
$stmt = $wlink->prepare('UPDATE xn_user SET balance=balance+? WHERE uid=?');
$stmt->execute([$amount, $toUid]);
// 记录流水
$stmt = $wlink->prepare('INSERT INTO xn_transfer (from_uid, to_uid, amount, created_at) VALUES (?,?,?,?)');
$stmt->execute([$fromUid, $toUid, $amount, XN_TIME]);
$wlink->commit();
return true;
} catch (Exception $e) {
$wlink->rollBack();
return false;
}
}常见问题
如何防止 SQL 注入?
始终使用db_find/db_insert/db_update/db_delete等封装函数,它们内部使用 PDO 参数绑定(?占位符),参数值不会拼接到 SQL 中。禁止将用户输入直接拼接到 SQL 字符串后传给db_exec或db_sql_find。条件数组的字段名为什么要白名单校验?
db_cond_to_sqladd使用正则/^[a-zA-Z_][a-zA-Z0-9_]*$/过滤字段名,非法字段会被静默跳过。这是为了防止用户可控字段名(如GET参数传入的orderby)注入任意 SQL。如果确实需要动态字段,务必在业务层做严格的字段名映射。主从切换是如何工作的?
db_pdo_mysql构造时读取配置中的master和slaves数组。读操作(SELECT)自动路由到随机选取的从库(rlink),写操作(INSERT/UPDATE/DELETE/DDL)路由到主库(wlink)。从库连接失败时自动回退到主库,不会中断服务。db_count返回 0 一定代表没有数据吗?
不一定。InnoDB 引擎无 WHERE 条件时会查information_schema.tables获取估算行数(速度快但不精确)。如果是 InnoDB 表且需要精确计数,建议使用带条件的查询或直接调用db_sql_find_one("SELECT COUNT(*) FROM ...")。如何处理大结果集?
避免一次性加载全部数据。使用db_find的分页参数分批获取;需要遍历全表时,配合LIMIT/OFFSET或使用游标。db_pdo_mysql默认开启缓冲查询,超大结果集请直接使用原生 PDO 关闭缓冲模式。

