ThinkPHP8学习篇(七):数据库(三)
在数据库操作环节,基础增删改查与简单条件查询已无法满足复杂业务需求,而聚合统计、分页展示、时间筛选、高级条件组合、子查询嵌套及原生SQL执行等能力,才是应对实际开发中多样数据处理场景的关键。本篇作为数据库系列文章的第三篇,学习核心内容将集中在聚合查询、分页查询的实现与参数配置、时间查询的常用方式、高级查询、子查询的构建与使用,以及原生查询的调用上。本篇文章将记录 ThinkPHP 数据库进阶查询能力的学习过程。
一、聚合查询
二、分页查询
1、简洁分页
2、分页参数
3、大数据分页
三、时间查询
1、时间比较
2、查询某个时间区间
3、查询某年/某月/某周/某天
4、时间字段区间比较
四、高级查询
1、快捷查询
2、批量(字段)查询
3、字符串条件查询
4、快捷方法
5、动态查询
6、条件查询
五、子查询
1、使用 fetchSql 方法
2、使用 buildSql 构造子查询
3、使用闭包构造子查询
六、原生查询
1、query 方法
2、execute 方法
一、聚合查询
在应用中我们经常会用到一些统计数据,例如当前所有(或者满足某些条件)的用户数、所有用户的最大积分、用户的平均成绩等等,ThinkPHP为这些统计操作提供了一系列的内置方法,包括:
方法 | 说明 |
count | 统计数量,参数是要统计的字段名(可选) |
max | 获取最大值,参数是要统计的字段名(必须) |
min | 获取最小值,参数是要统计的字段名(必须) |
avg | 获取平均值,参数是要统计的字段名(必须) |
sum | 获取总分,参数是要统计的字段名(必须) |
聚合方法如果没有数据,默认都是0,聚合查询都可以配合其它查询条件。
示例
// 1、获取 user 表数据总条数,使用 count 方法
Db::table('user')->count();
// 生成的SQL语句为:SELECT COUNT(*) AS think_count FROM `user`// 2、根据字段统计表中数据总量,使用 count 方法并传入字段
Db::table('user')->count('id');
// 生成的SQL语句为:SELECT COUNT(`id`) AS think_count FROM `user`// 3、获取 user 表中 age 字段的最大值,使用 max 方法
Db::table('user')->max('age');
// 生成的SQL语句为:SELECT MAX(`age`) AS think_max FROM `user`
在这里列举了 count、max、min 三个方法的使用示例,其它方法使用与这三个方法是一样的,这里不再一一进行演示。
如果要使用 group 进行聚合查询,需要自己实现查询。例如:
Db::table('score')->field('user_id,SUM(score) AS sum_score')->group('user_id')->select();
二、分页查询
ThinkPHP 内置了分页实现,要给数据添加分页输出功能变得非常简单,可以直接在 Db 类查询的时候调用 paginate 方法。与之前使用的分页 page 方法不同,paginate 方法查询的内容可以作用在模板文件中,会自动的在模板中生成分页相关内容,可以进行分页操作。
示例
// 查询 user 表数据,根据 id 降序排序,每页显示 1 条数据(方便分页展示)
$list = Db::name('user')->order('id', 'desc')->paginate(1);// 渲染模板输出
return view('index', ['list' => $list]);
模板文件中分页输出代码如下:
<div><ul>{volist name='list' id='user'}<li> {$user.name}</li>{/volist}</ul>
</div>
<!-- 添加此行代码,将在页面中生成分页信息 -->
{$list|raw}
效果:
通过执行后的效果可以看到,页面中生成了分页信息的相关展示,此时点击页码可以跳转到页码指定页,后台不需要做任何的更改。
通过这个示例可以看到,paginate 方法进行分页十分简单,不需要像以往的 web 开发那样,需要在后台代码中获取当前页码等操作。
默认情况下,生成的分页输出是完整分页功能,带总分页数据和上下页码,分页样式只需要通过样式修改即可,完整分页默认生成的分页输出代码为:
<!-- 默认生成的分页代码 -->
<ul class="pagination"><li><a href="?page=1">«</a></li><li><a href="?page=1">1</a></li><li class="active"><span>2</span></li><li class="disabled"><span>»</span></li>
</ul>
如果需要获取数据总量,即一共多少条数据,可以这样做:
$list = Db::name('user')->order('id', 'desc')->paginate(1);
// 获取总记录数
$count = $list->total();
// 此时总记录数 count 渲染到模板,可以在模板中对其进行展示
return view('index', ['list' => $list, 'count' => $count]);
paginate 方法也支持传入总记录数。当传入总记录数后,paginate 方法将不会自动进行总数计算。例如:
// 总记录数为 50,paginate(每页显示数据条数, 数据总记录数)
$list = Db::name('user')->order('id', 'desc')->paginate(1, 50);
return view('index', ['list' => $list]);
官网建议:对于 UNION 查询以及一些特殊的复杂查询,推荐使用这种方式首先单独查询总记录数,然后再传入分页方法。
支持分页类后数据直接 each 遍历处理,方便修改分页后的数据。
// 将 name 字段的值更改为大写
$list = Db::name('user')->order('id', 'desc')->paginate(1)->each(function($item, $key) {$item['name'] = strtoupper($item['name']);return $item;
});
1、简洁分页
如果不想输出全部的分页页码,仅仅只有上下页的分页展示,可以使用下面的简洁分页代码:
// paginate 方法第二个参数传入 ture 表示使用简洁分页
$list = Db::name('user')->order('id', 'desc')->paginate(1, true);
return view('index', ['list' => $list]);
简洁分页模式的输出代码为:
<ul class="pager"><li><a href="?page=1">«</a></li><li class="disabled"><span>»</span></li>
</ul>
页面显示效果:
由于简洁分页模式不需要查询总数据数,因此可以提高查询性能。
2、分页参数
主要的分页参数如下表所示:
参数 | 描述 |
list_rows | 每页数量 |
page | 当前页 |
path | url路径 |
query | url额外参数 |
fragment | url锚点 |
var_page | 分页变量 |
分页参数的设置可以在调用分页方法的时候传入,例如:
$list = Db::name('user')->paginate(['list_rows'=> 20,'var_page' => 'page',
]);
如果需要在分页的时候传入查询条件,可以使用 query 参数拼接额外的查询参数。
3、大数据分页
对于大数据量的分页查询,系统提供了一个高性能的 paginateX 分页查询方法,用法和 paginate 分页查询存在一定区别。如果要分页查询的数据量在百万级以上,使用 paginateX 方法会有明显的提升,尤其是在分页数较大的情况下。并且由于针对大数据量而设计,该分页查询只能采用简洁分页模式,所以没有总数。
分页查询的排序字段一定要使用索引字段,并且是连续的整型,否则会有数据遗漏。
主要场景是针对主键进行分页查询,默认使用主键倒序查询分页数据。
$list = Db::name('user')->paginateX(1);
也可以在查询的时候可以指定主键和排序。
// paginateX(每页显示数据条数, 主键, 排序方式)
$list = Db::name('user')->paginateX(1, 'id', 'asc');
查询方法会执行两次查询,第一次查询用于查找满足当前查询条件的最大或者最小值,然后配合主键查询条件来进行分页数据查询。
三、时间查询
1、时间比较
框架内置了常用的时间查询方法,并且可以自动识别时间字段的类型,所以无论采用什么类型的时间字段,都可以统一使用时间查询方法。
whereTime 方法提供了日期和时间字段的快捷查询。
示例
// 大于某个时间
Db::name('user')->whereTime('birthday', '>=', '1970-10-1')->select();
// 时间区间查询
Db::name('user')->whereTime('birthday', 'between', ['1970-10-1', '2000-10-1'])->select();
还可以使用下面的时间表达式进行时间查询:
// 查询两个小时内的注册的用户
Db::name('user')->whereTime('create_time','-2 hours')->select();
2、查询某个时间区间
针对时间的区间查询,系统提供了 whereBetweenTime / whereNotBetweenTime 快捷方法。
示例
// 查询2025年上半年注册的用户
Db::name('user')->whereBetweenTime('create_time', '2025-01-01', '2025-06-30')->select();
// 查询不是2025年上半年注册的用户
Db::name('user')->whereNotBetweenTime('create_time', '2025-01-01', '2025-06-30')->select();
3、查询某年/某月/某周/某天
// 查询今年注册的用户
Db::name('user')->whereYear('create_time')->select();
// 查询去年注册的用户
Db::name('user')->whereYear('create_time', 'last year')->select();
// 查询某一年的数据
Db::name('user')->whereYear('create_time', '2018') // 传入具体年份->select();
类似 whereYear 的方法还有:whereMonth 方法查询某月,whereWeek 方法查询某周,whereDay 方法查询某天。
4、时间字段区间比较
whereBetweenTimeField 方法可以支持对两个时间字段的区间比较。
示例
Db::name('event')->whereBetweenTimeField('start_time', 'end_time')->select();
// 上面的查询相当于
Db::name('event')->whereTime('start_time', '<=', time())->whereTime('end_time', '>=', time())->select();
四、高级查询
1、快捷查询
快捷查询方式是一种多字段相同查询条件的简化写法,可以进一步简化查询条件的写法,在多个字段之间用 | 分割表示 OR 查询,用 & 分割表示 AND 查询。
示例
Db::table('user')->where('name|title','like','thinkphp%')->where('create_time&update_time','>',0)->select();
// 生成的SQL语句为:SELECT * FROM `user` WHERE ( `name` LIKE 'thinkphp%' OR `title` LIKE 'thinkphp%' ) AND ( `create_time` > 0 AND `update_time` > 0 )
快捷查询支持所有的查询表达式。
2、批量(字段)查询
可以进行多个条件的批量条件查询定义,例如:
Db::table('user')->where([['name', 'like', 'thinkphp%'],['title', 'like', '%thinkphp'],['id', '>', 0],['status', '=', 1],])->select();
// 生成的SQL语句为:SELECT * FROM `user` WHERE `name` LIKE 'thinkphp%' AND `title` LIKE '%thinkphp' AND `id` > 0 AND `status` = '1'
数组方式如果使用 exp 查询的话,一定要用 raw 方法。
示例
Db::table('user')->where([['name', 'like', 'thinkphp%'],['title', 'like', '%thinkphp'],['id', 'exp', Db::raw('>score')],['status', '=', 1],])->select();
如果希望某一个 where 方法里面的条件单独处理,可以使用下面的方式,避免被其它条件影响。
$map = [['name', 'like', 'thinkphp%'],['title', 'like', '%thinkphp'],['id', '>', 0],];
Db::table('user')->where([ $map ])->where('status',1)->select();
// 生成的SQL语句为:SELECT * FROM `user` WHERE ( `name` LIKE 'thinkphp%' AND `title` LIKE '%thinkphp' AND `id` > 0 ) AND `status` = '1'
// 从生成的SQL语句中可以看到,map 定义的条件被 () 包起来了
也可以像下面这样使用多个条件组合:
$map1 = [['name', 'like', 'thinkphp%'],['title', 'like', '%thinkphp'],];$map2 = [['name', 'like', 'kancloud%'],['title', 'like', '%kancloud'],]; Db::table('user')->whereOr([ $map1, $map2 ])->select();
// 生成的SQL语句为:SELECT * FROM `user` WHERE ( `name` LIKE 'thinkphp%' AND `title` LIKE '%thinkphp' ) OR ( `name` LIKE 'kancloud%' AND `title` LIKE '%kancloud' )
// 从生成的SQL语句中可以看到,每个被定义的条件都被 () 抱起来了
善用多维数组查询,可以很方便的拼装出各种复杂的SQL语句。
3、字符串条件查询
对于一些复杂的查询,也可以直接使用原生SQL语句进行查询。
示例
Db::table('user')->whereRaw('id > 0 AND name LIKE "thinkphp%"')->select();
为了安全起见,我们可以对字符串查询条件使用参数绑定,例如:
Db::table('user')->whereRaw('id > :id AND name LIKE :name ', ['id' => 0, 'name' => 'thinkphp%'])->select();
4、快捷方法
系统封装了一系列快捷方法,用于简化查询,包括:
方法 | 作用 |
whereOr | 字段OR查询 |
whereXor | 字段XOR查询 |
whereNull | 查询字段是否为Null |
whereNotNull | 查询字段是否不为Null |
whereIn | 字段IN查询 |
whereNotIn | 字段NOT IN查询 |
whereBetween | 字段BETWEEN查询 |
whereNotBetween | 字段NOT BETWEEN查询 |
whereLike | 字段LIKE查询 |
whereNotLike | 字段NOT LIKE查询 |
whereExists | EXISTS条件查询 |
whereNotExists | NOT EXISTS条件查询 |
whereExp | 表达式查询 |
whereColumn | 比较两个字段 |
示例 whereColumn 方法:两个字段比较的查询条件
// 查询 update_time 大于 create_time 的用户数据
Db::table('user')->whereColumn('update_time','>','create_time')->select();
// 生成的SQL语句为:SELECT * FROM `user` WHERE ( `update_time` > `create_time` ) // 支持数组方式比较多个字段
Db::name('user')->whereColumn([['title', '=', 'name'],['update_time', '>=', 'create_time'],
])->select();
// SELECT * FROM `user` WHERE ( `name` = `nickname` AND `update_time` > `create_time` )
5、动态查询
查询构造器还提供了动态查询机制,用于简化查询条件,包括:
动态查询 | 描述 |
whereFieldName | 查询某个字段的值 |
whereOrFieldName | 查询某个字段的值 |
getByFieldName | 根据某个字段查询 |
getFieldByFieldName | 根据某个字段获取某个值 |
其中 FieldName 表示数据表的实际字段名称的驼峰法表示。
假设数据表 user 中有 email 和 nick_name 字段,我们可以这样来查询。
// 根据邮箱(email)查询用户信息
$user = Db::table('user')->whereEmail('thinkphp@qq.com') // whereEmail 中的 Email 就是数据表 user 中的 email 字段的驼峰表示法->find();// 根据昵称(nick_name)查询用户
$email = Db::table('user')->whereNickName('like', '%流年%') // whereNickName 中的 NickName 就是数据表 user 中的 nick_name 字段的驼峰表示法->select();
其它方法的使用同示例所示方法是一样的。
6、条件查询
查询构造器支持条件查询。
示例
Db::name('user')->when($condition, function ($query) {// 满足 $condition 条件后执行,否则不执行$query->where('score', '>', 80)->limit(10);
})->select();
// 执行说明:
// 1、$condition 为 True,生成的SQL语句为:SELECT * FROM `user` WHERE `score` > 80 LIMIT 10
// 2、$condition 为 False,生成的SQL语句为:SELECT * FROM `user`
并且支持不满足条件的分支查询:
Db::name('user')->when($condition, function ($query) {// 满足 $condition 条件后执行$query->where('score', '>', 80)->limit(10);
}, function ($query) {// 不满足 $condition 条件执行$query->where('score', '>', 60);
});
五、子查询
首先构造子查询SQL,可以使用以下三种方式来构建子查询。
1、使用 fetchSql 方法
fetchSql 方法表示不进行查询而只是返回构建的SQL语句,并且不仅仅支持 select,而是支持所有的CURD查询。
示例
$subQuery = Db::table('user')->field('id,name')->where('id', '>', 10)->fetchSql(true)->select();
// 生成的subQuery结果为:SELECT `id`,`name` FROM `user` WHERE `id` > 10
2、使用 buildSql 构造子查询
$subQuery = Db::table('user')->field('id,name')->where('id', '>', 10)->buildSql();
// 生成的subQuery结果为:( SELECT `id`,`name` FROM `user` WHERE `id` > 10 )
调用 buildSql 方法后不会进行实际的查询操作,而只是生成该次查询的SQL语句(为了避免混淆,会在SQL两边加上括号),然后我们直接在后续的查询中调用。
然后使用子查询构造新的查询:
Db::table($subQuery . ' a')->where('a.name', 'like', 'zhangsan')->order('id', 'desc')->select();
// 生成的SQL语句为:SELECT * FROM ( SELECT `id`,`name` FROM `user` WHERE `id` > 10 ) a WHERE `a`.`name` LIKE 'zhangsan' ORDER BY `id` DESC
3、使用闭包构造子查询
IN/NOT IN 和 EXISTS/NOT EXISTS 之类的查询可以直接使用闭包作为子查询。
示例
Db::table('user')->where('id', 'IN', function ($query) {$query->table('role')->where('status', 1)->field('id');
})
->select();
// 生成的SQL语句为:SELECT * FROM `user` WHERE `id` IN (SELECT `id` FROM `role` WHERE `status` = 1)
除了上述查询条件外,比较运算也支持使用闭包子查询。
六、原生查询
Db 类支持原生SQL查询操作,主要包括 query 和 execute 两个方法。
1、query 方法
query 方法用于执行SQL查询操作,返回查询结果数据集(数组)。
示例
Db::query('select * from user where id=:id', ['id' => 1])
2、execute 方法
execute 用于更新和写入数据的sql操作,支持在原生查询的时候使用参数绑定,包括问号占位符或者命名占位符。如果数据非法或者查询错误则返回 false,否则返回影响的记录数。
示例
// 问号占位符
Db::execute('update user set name=? where id=?', ['wangwu', 1]);
// 命名占位符
Db::execute('update user set name=:name where id=:id', ['name' => 'zhangsan', 'id' => 1]);
注意:不支持对表名使用参数绑定。