查询构造器

查询构造器(Query Builder)提供链式 API 构建并执行参数化 SQL,是 Db::table() 的返回对象(Viswoole\Database\BaseQuery)。所有值经参数绑定写入数据库,条件运算符与联表运算符均做白名单校验,防止 SQL 注入。

本文依据框架源码 src/Database/BaseQuery.php、src/Database/Query/{Crud,Where,WhereGroup,Join,Options}.php 与 src/Database/Facade/Db.php 编写。

获取查询构造器

php
use Viswoole\Database\Facade\Db;

// 默认通道
Db::table('user');               // 第二参数可指定主键名,默认 'id'

// 指定通道
Db::channel('order')->table('order');

查询执行后构造器会自动重置条件并复用实例;如需在保留原实例的前提下构建新查询,调用 newQuery() 克隆一个全新实例。

查询数据

php
use Viswoole\Database\Facade\Db;

// 主键查询,返回 DataSet 单行数据集;未查到返回 null
$user = Db::table('user')->find(1);
if ($user === null) {
    // 未查到记录
}

// find 第二参数传 false:未查到时抛出 DataNotFoundException
$user = Db::table('user')->find(1, false);

// 多条查询,返回 Collection 集合
$list = Db::table('user')->where('status', 1)->select();

// select(false) / get(false):结果为空时抛出 DataNotFoundException
// get() 是 select() 的别名;getArray() 以纯数组返回结果
$array = Db::table('user')->where('status', 1)->getArray();

// value():取第一行指定列的值(自动 LIMIT 1),无结果返回 false
$name = Db::table('user')->where('id', 1)->value('name');

// exists():判断是否存在满足条件的数据;doesntExist() 为其取反快捷方法
if (Db::table('user')->where('email', 'a@example.com')->exists()) {
    // 记录存在
}
if (Db::table('blacklist')->where('ip', $ip)->doesntExist()) {
    // 记录不存在
}

exists 的实现与注意点

exists() 生成 SELECT 1 ... LIMIT 1 只取一行单列哨兵,命中即真,比 count() 全表计数高效。使用时注意:

  • 若已通过 columns() 设置查询列,SELECT 1 会叠加在其后(判空结果不受影响);
  • 查询缓存键不可与普通 select() 复用——exists() 先执行会把单列哨兵行写入缓存,同 key 的 select() 命中后返回错误形态的数据;
  • 模型查询中 with() 注册的关联不会跳过,判空前会照常执行关联查询(无谓开销),判空场景建议不携带 with()。

查询结果为 Collection(多行)与 DataSet(单行)集合对象,支持数组式访问、toArray()、filter()、sortBy() 等操作,详见 ORM 模型 — 查询结果。

条件查询

where 系列

where(string $column, string|int|float|array $operator, string|int|float|array|null $value = null, string $connector = 'AND') 支持两种写法:

php
// 两参简写:默认 = 运算符;值传数组时自动转为 IN
->where('status', 1)
->where('type', [1, 2])            // 等价 type IN (1, 2)

// 三参写法:显式指定运算符
->where('age', '>=', 18)

// 第四参数指定连接符 AND | OR
->where('age', '>=', 18, 'OR')

条件运算符白名单(Where::OPERATORS),传入其他运算符抛出 InvalidArgumentException:

  • 比较运算符:=、!=、<>、>、>=、<、<=、LIKE
  • 集合运算符:IN、NOT IN
  • 区间运算符:BETWEEN、NOT BETWEEN
  • 空值判断:IS NULL、IS NOT NULL

其余条件方法(均支持末位 $connector = 'AND' 参数):

方法说明
orWhere($column, $operator, $value = null)OR 连接的条件(andWhere() 为显式 AND)
whereIn($column, array $value)IN 条件,空数组抛异常
whereNotIn($column, array $value)NOT IN 条件,空数组抛异常
whereNull($column)IS NULL
whereNotNull($column)IS NOT NULL
whereBetween($column, array $value)BETWEEN 区间(两个元素)
whereNotBetween($column, array $value)NOT BETWEEN 区间
wheres(array $wheres)批量设置条件,见下文
php
// wheres():键值对默认 = / 数组默认 IN;
// 索引数组形式 ['字段', '运算符', '值', '连接符?'] 支持任意白名单运算符
->wheres([
    'status' => 1,
    'type' => [1, 2],
    ['age', '>=', 18],
    ['role', '=', 'admin', 'OR'],
])

whereGroup 条件分组

whereGroup(array $wheres, string $connector = 'AND') 将一组条件用括号包裹,构建 (A AND B) OR C 等复杂逻辑,组内条件格式与 wheres() 一致:

php
Db::table('user')
    ->where('type', 'vip')
    ->whereGroup([
        ['status', '=', 1],
        ['role', '=', 'admin', 'OR'],
        ['role', '=', 'super_admin', 'OR'],
    ], 'AND')
    ->select();
// WHERE type = 'vip' AND (status = 1 AND role = 'admin' OR role = 'super_admin')

原生条件与 EXISTS

php
// whereRaw(string $sql, array $bindings = []):原生片段,占位符参数绑定
->whereRaw('DATE(create_time) = ?', ['2026-01-01'])

// whereExists(string $sql, array $bindings = []):EXISTS 子查询
->whereExists('SELECT 1 FROM orders WHERE orders.user_id = user.id')

// whereNotExists(...):NOT EXISTS 子查询

构造选项

字段与别名

php
// columns(string ...$column):查询字段,支持 "列 AS 别名";不传则查全部
->columns('id', 'name', 'COUNT(*) AS total')

// withoutColumns(string ...$column):排除字段
->withoutColumns('password', 'pay_password')

// alias(string $alias):表别名;join 多表时配合别名区分字段
Db::table('user')->alias('u')->columns('u.id', 'u.name')->select();

// distinct(bool $flag = true):DISTINCT 去重
// force(string $index):强制索引 FORCE INDEX(仅 MySQL / SQLite 有效)

排序、分组与分页

php
// orderBy:三种形式
->orderBy('create_time', 'desc')                    // 单列
->orderBy(['sort' => 'asc', 'age' => 'desc'])       // 多列各自方向
->orderBy(Db::raw('RAND()'))                        // 原生表达式

// groupBy(string|array $columns):分组,字符串可逗号分隔
->groupBy('user_id')

// having(string $column, string $operator, mixed $value, string $connector = 'AND')
// 运算符白名单:= != <> > >= < <= LIKE
->having('order_count', '>', 5)

// page(int $page, int $pageSize):分页(页码从 1 开始),等价 offset + limit
->page(2, 20)

// limit(int) / offset(int):底层方法,传入负数抛出 InvalidArgumentException

联表查询

join(string $table, string $localKey, string $foreignKey, string $operator = '=', string $type = 'INNER'),类型支持 INNER / LEFT / RIGHT / FULL;另有 leftJoin()、rightJoin()、fullJoin() 快捷方法。ON 条件运算符白名单:= != <> > >= < <= LIKE。

php
Db::table('user u')
    ->leftJoin('order o', 'u.id', 'o.user_id')   // 注意第 3 参是外键、第 4 参是运算符
    ->columns('u.name', 'o.amount')
    ->select();

leftJoin 参数顺序已变更

leftJoin 签名为 (table, localKey, foreignKey, operator)。若第 3 个参数收到运算符(旧版参数顺序 (table, localKey, operator, foreignKey) 的写法),框架会直接抛出带提示的异常,请按新顺序调整调用。

其他选项

方法说明
strict(bool $flag = true)严格模式:写入不存在的字段抛异常;关闭则忽略
`union(stringRaw $query, string $type = ‘UNION’)`
lockForUpdate()排他锁 FOR UPDATE,需在事务内使用
sharedLock()共享锁 LOCK IN SHARE MODE
replace(bool $flag = true)写入改用 REPLACE INTO(仅 MySQL),与 duplicate() 互斥
toRaw()仅生成 SQL 不执行,返回 Raw 对象(调试 SQL 用)

聚合查询

方法签名说明
count()count(string $column = '*')计数
min()min(string $column)最小值
max()max(string $column)最大值
avg()avg(string $column)平均值
sum()sum(string $column)求和
php
$total    = Db::table('user')->where('status', 1)->count();
$totalSum = Db::table('order')->sum('amount');

聚合返回值归一

avg() / sum() / min() / max() 的返回值已做归一:空表(无聚合结果)返回 null;is_numeric 的结果转换为 int|float(修复 MySQL 将整型列 AVG 等聚合结果以字符串返回的问题),非数值原样返回。count() 始终返回 int。

聚合字段校验

聚合字段仅允许字母、数字、下划线与点号(或 *),防止用户输入被拼入聚合表达式造成注入。

写入与删除

php
use Viswoole\Database\Facade\Db;

// insert:关联数组单条写入,索引数组批量写入;返回受影响行数
Db::table('user')->insert(['name' => '张三', 'status' => 1]);

// insertGetId:仅接受关联数组,返回自增主键值
$id = Db::table('user')->insertGetId(['name' => '张三']);

// update:返回受影响行数;值支持 Raw 表达式
Db::table('user')->where('id', 1)->update([
    'login_count' => Db::raw('login_count + 1'),
]);

// delete:配合 where 条件删除,返回受影响行数
Db::table('user')->whereIn('id', [10, 11])->delete();

Db::raw(string $sql, array $bindings = []) 创建原生 SQL 表达式,值不会被参数绑定转义,适合 NOW()、字段自增等数据库端表达式。

increment/decrement 原子自增自减

increment(string $column, int|float $amount = 1, array $extra = []) 与 decrement(...) 生成 SET col = col ± ? 原子更新语句——增量走参数绑定,直接在数据库端完成加减,无需先读后写,并发场景下不会丢失更新:

php
// login_count 自增 1
Db::table('user')->where('id', 1)->increment('login_count');

// score 自减 5
Db::table('user')->where('id', 1)->decrement('score', 5);

// 自增同时写入其他字段
Db::table('user')->where('id', 1)->increment('score', 5, ['level' => 2]);

使用要点:

  • 与 update() 一样需要 where 条件(或数据中包含主键值);返回受影响的记录数;
  • $amount 传负值等价于反向操作(increment 传 -1 等价 decrement 传 1);
  • $extra 不能包含自增列名,否则抛出 InvalidArgumentException——同名键会整体覆盖自增表达式,静默丢失原子语义;
  • 列名仅允许字母、数字、下划线与点号(白名单校验),防止原生片段注入;
  • 模型查询(update_time 等自动时间戳写入)照常生效,见 ORM 模型;

与 update + Raw 的区别

update(['login_count' => Db::raw('login_count + 1')]) 同样是数据库端表达式,但值不经过参数绑定;increment()/decrement() 将增量作为绑定参数处理,且对 extra 与自增列同名、非法列名做了显式拦截,语义更安全,优先使用。

duplicate 唯一键冲突更新

duplicate(array $data, string $rowAlias = '') 为写入语句追加 ON DUPLICATE KEY UPDATE 子句:唯一键不冲突时正常插入,冲突时改为更新指定列:

php
Db::table('user')
    ->duplicate(['score' => 10])
    ->insert(['name' => '张三', 'score' => 1]);
// INSERT INTO `user` (`name`, `score`) VALUES (?, ?)
//   ON DUPLICATE KEY UPDATE `score` = ?

更新值支持原生表达式,典型的「存在即自增」场景:

php
Db::table('user')
    ->duplicate(['login_count' => Db::raw('login_count + 1')])
    ->insert(['name' => '张三']);
// ON DUPLICATE KEY UPDATE `login_count` = login_count + 1

传入 $rowAlias(MySQL 8.0.19+)生成 VALUES (...) AS 别名 行别名,更新子句可用 别名.列 引用本次待插入的值(替代 MySQL 8.0.20 起废弃的 VALUES(col) 写法):

php
Db::table('sys_platform_configs')
    ->duplicate([
        'config_value' => Db::raw('new.config_value'),
        'update_time'  => Db::raw('new.update_time'),
    ], rowAlias: 'new')
    ->insert([
        'config_key'   => 'site_name',
        'config_value' => 'viswoole',
        'update_time'  => '2026-10-08 00:00:00',
    ]);
// INSERT INTO `sys_platform_configs` (...) VALUES (?, ?, ?) AS `new`
//   ON DUPLICATE KEY UPDATE `config_value` = new.config_value,
//                          `update_time` = new.update_time

仅 MySQL 有效且与 replace() 互斥

非 MySQL 驱动(PostgreSQL / SQLite 等使用 ON CONFLICT 语法)在构建 SQL 时会直接抛出异常,请改用原生 SQL;duplicate() 与 replace() 不可同时使用,两种调用顺序均会在链式调用点被拦截。此外 duplicate() 仅对 insert() / insertGetId() / create() 生效,以 update() / delete() 收尾时已设置的更新数据会被静默忽略。

返回值语义

MySQL 的 ODKU 行为与普通 insert 不同,请勿按写入条数解读:insert() 返回的受影响行数为 0(冲突且值未变)/ 1(新插入)/ 2(冲突更新);insertGetId() / create() 在命中冲突更新路径时,LAST_INSERT_ID 返回的是该连接上一次成功插入的自增值(连接池复用场景下可能是过期值)。需要可靠取回冲突行主键时,使用惯用法:

php
Db::table('user')
    ->duplicate(['id' => Db::raw('LAST_INSERT_ID(id)')])
    ->insert(['name' => '张三']);
// ON DUPLICATE KEY UPDATE `id` = LAST_INSERT_ID(id)

Db::raw() 的内容是赋值右侧的表达式,框架会自动拼接 id = 前缀,勿在 Raw 中自带 id =,否则会生成双重赋值导致主键被覆写。

大结果集

php
// cursor():游标查询,逐条返回 DataSet 的生成器,内存占用恒定
foreach (Db::table('user')->cursor() as $user) {
    // 处理单条记录
}

// chunk(int $size):每批查询 $size 条,逐批产出 Collection 的生成器
foreach (Db::table('user')->chunk(1000) as $collection) {
    // 处理一批记录
}

使用场景

两者均绕过查询缓存、自动翻页。一次性处理十万级以上数据时,避免用 select() 把全量结果载入内存。

查询缓存

cache(string $key, int $expire = 0, ?string $tag = null, ?string $store = null) 为 SELECT 开启结果缓存:命中直接返回缓存,未命中则查询后写入;执行写入操作时自动清除对应缓存。

参数类型默认值说明
keystring必填缓存键
expireint0有效期(秒),0 表示永不过期
tagstring | nullnull缓存标签,便于按组清理
storestring | nullnull缓存存储器名称,null 使用默认存储器
php
$users = Db::table('user')
    ->where('status', 1)
    ->cache('user_list', 600, 'user')   // 10 分钟,标签 user
    ->select();

缓存依赖

缓存由缓存组件驱动,存储器与标签的配置见 缓存。cursor() 与 chunk() 会强制关闭缓存。

事务

php
use Viswoole\Database\Facade\Db;

// 方式一(推荐):闭包事务,成功自动 commit,异常自动 rollback 并重抛,返回闭包返回值
$newId = Db::startTransaction(function () {
    $id = Db::table('user')->insertGetId(['name' => '张三']);
    Db::table('log')->insert(['action' => 'create_user', 'user_id' => $id]);
    return $id;
});

// 方式二:手动管理
Db::startTransaction();   // 开启事务(别名 Db::start())
try {
    Db::table('user')->insert(['name' => '张三']);
    Db::commit();
} catch (\Throwable $e) {
    Db::rollBack();
    throw $e;
}

嵌套事务

同一协程内事务支持多层嵌套,内层基于数据库 SAVEPOINT 实现:

  • commit() / rollBack() 始终作用于当前最内层事务,需按后开先关(LIFO)顺序收尾;
  • 内层回滚仅回滚到该层保存点,外层操作不受影响;内层提交仅释放保存点,数据仍受最外层事务保护;
  • Db::transactionLevel(): int 返回当前嵌套层级,0 表示无事务。
php
Db::startTransaction(function () {              // 外层
    Db::table('user')->insert(['name' => '张三']);
    try {
        Db::startTransaction(function () {      // 内层(SAVEPOINT)
            throw new \RuntimeException('失败');
        });
    } catch (\RuntimeException) {
        // 仅内层回滚,外层事务可继续
    }
    Db::table('log')->insert(['action' => 'ok']);
});

事务边界

事务基于协程上下文隔离,不同协程各自开启的事务互不影响。跨通道/跨库事务不保证原子性(无两阶段提交),需要跨库原子性的业务请使用 XA 事务。

原生查询

php
use Viswoole\Database\Facade\Db;

// query:执行查询语句,返回关联数组结果集;$master 为 true 强制从主库读取
$users = Db::query('SELECT * FROM user WHERE status = ?', [1]);

// execute:执行写入语句,返回受影响行数;传字段名时返回自增 ID
$count = Db::execute('UPDATE user SET status = ? WHERE id = ?', [0, 1]);
$newId = Db::execute('INSERT INTO user (name) VALUES (?)', ['张三'], 'id');

query() 仅接受查询语句

query() 只接受 SELECT / SHOW / EXPLAIN / DESCRIBE / DESC / WITH / PRAGMA 开头的语句,写入语句会抛出 DbException,请改用 execute()。

下一步