ThinkPHP5如何实现分页查询_ThinkPHP5实现分页查询方法【代码】
ThinkPHP5使用paginate()分页:必须作为查询链最后方法;参数名不匹配需手动指定;数据量大可跳过COUNT防性能瓶颈;通过items()和render()获取数据与HTML,避免误用。
先聊几句分页这事儿。很多人在ThinkPHP5里做分页,第一步就是去查paginate()的用法,照着文档写个例子,跑起来看着也没毛病。但稍微变个场景——比如URL参数名不一样了、表数据上了百万、或者想自己控制一下分页的行为——就开始出各种奇怪的问题。说到底,分页本身不复杂,真正考验人的,是对这个“黑盒”的理解有多深。
先说最常规的场景。ThinkPHP5里用paginate()做分页,确实是最省事的方式,它自动帮你处理SQL的LIMIT和OFFSET,还能顺手生成分页的HTML代码。但这里有几个容易被忽略的关键点,一旦没注意,报错信息会非常让人摸不着头脑。
paginate()必须是查询链上的最后一个方法。别想着在where()之前就调它,也别跟在find()、select()后面再调。严格顺序,否则直接报错。- 参数直接传数字,表示每页显示多少条,比如
paginate(15)。如果你需要更精细地控制,可以传一个数组,比如paginate(['list_rows'=>20, 'page'=>3])。注意,数组的键名是有规定的,不是随便写。 - 在调用
paginate()之前如果用了field(),记得把主键字段带上(通常是id)。否则,paginate()在统计总数时可能会因为找不到主键而出错。
// 正确写法:先查状态为1的用户,按创建时间倒序,每页10条
$users = User::where('status', 1)
->order('create_time DESC')
->paginate(10);
// 模板中输出分页HTML,默认是Bootstrap风格
echo $users->render();
手动传入当前页码时,得搞清楚参数名
这是另一个高频坑点。ThinkPHP5默认从$_GET['page']里读当前页码,但现实项目里,带翻页的URL往往是/list?page_no=2或者用了路由变成了/list/p/2。如果没做配置,paginate()始终读到的是null,自然永远显示第一页。
解决办法其实也不复杂,分两种情况:
- 如果是GET参数名不一样,可以用第三个参数指定:
paginate(10, false, ['page' => 'page_no'])。第二个参数false的意思是“不自动获取总记录数”,一般情况下不用动它。 - 如果用了URL路由(比如
list/p/:page),就需要配合input('param.page')手动传页码进去:paginate(10, false, ['page' => input('param.page', 1)])。
必须警惕的是,不要在控制器里自己先input('page'),然后把取到的值再传给paginate()。这么做等于绕过了TP5自带的页码校验逻辑,如果传入的是负数或者非数字,SQL就会报异常,这是一个非常隐蔽的坑。
从分页结果对象里取数据,方式和取分页信息要分开
又有新人会踩的雷:$list = User::paginate(10)之后,直接foreach($list as $u),结果报错Invalid argument supplied for foreach()。原因很简单,$list不是一个数组,它是一个Paginator对象。你得用对方法才能拿到里面的东西。
- 取当前页的数据:用
$list->items(),返回的是一个Collection或数组。 - 取分页的HTML代码:用
$list->render(),返回的是字符串。 - 取总条数:用
$list->total()。千万别写成count($list),那个数的是当前页的数据条数,不是总记录数。 - 模板里输出分页链接时,常见写法是
{$data->render|raw},一定要加上|raw,否则HTML会被转义成纯文本,什么都看不到。
数据量大的时候,count()是性能瓶颈
当表里数据到了百万级别,又没有合适的索引时,paginate()默认执行的COUNT(*)可能会耗时好几秒。说到底,这锅真不该让TP5来背,是MySQL本身的查询优化问题。在数据量大到一定程度后,强行优化count()不如换个思路。
- 直接在
paginate()的第二个参数传false来跳过count:paginate(15, false)。这样之后,$list->lastPage()和$list->total()是获取不到值的,但hasMore仍然可用——系统会通过判断是否能查到下一页的数据来确定。 - 前端显示“共 ? 条”的位置,可以留空,或者直接改成“仅显示前N条”。从用户体验角度看,这种诚实反而更友好。
- 如果确实需要总数,可以单独维护一个缓存里的近似值,比如用定时任务定期更新,没必要每次请求都去执行一次全表COUNT。
最后必须说的是,分页这个功能本身确实不复杂。但真正考验人的,是对paginate()这个“黑盒”的理解:它的count行为、参数解析机制、以及与URL参数的耦合关系,全都藏在think\Paginator和think\db\Query的源码里。改配置的时候,顺手去翻一下源码里的注释,很多问题其实都能提前避免。
