这篇文章会对 Rails 中的分页做个简单的介绍。利用Kaminari能够很简单的实现分页功能,然而它并不是万能的,有一些场景它无法很好地满足业务需求。针对这种场景我会在本篇提出一个简单的“权宜之计”。
在 Rails 项目里面我们一般会采用Kaminari来实现分页功能。得益于 Rails 那优秀的生态,我们几乎不用费什么力气就能实现分页。
User.page(params[:page]).per(params[:per_page])
假设参数中page
为 10, per_page
为 2,它会直接利用了数据库的数据分割机制,每一页的数据为两条,然后获取第 10 页的数据。转换成数据库查询语句大概是这样子
SELECT "users".* FROM "users" LIMIT 2 OFFSET 9
除此之外Kaminari还会为查询结果提供常见的元数据,比如当前页数,总页数,数据总数等
> @users = User.page(1).per(2)
> @users.current_page # 当前页
=> 1
> @users.limit_value # 每页面包含数据
=> 2
> @users.total_pages # 总页数
=> 22
> @users.total_count # 总条数
=> 44
利用Kaminari提供的工具函数,编写有分页功能的 API 接口是如此地简单
# app/controllers/api/users_controller.rb
module Api
class UsersController < ApplicationController
def index
@users = User.page(params[:page]).per(params[:per_page])
end
end
end
# app/views/api/users/index.json.rabl
child @users => :data do
attributes :id, :nickname, :openid
end
child(:meta) do
node(:total_pages) { @users.total_pages }
node(:current_page) { @users.current_page }
node(:total_count) { @users.total_count }
end
我是用了rabl来做 JSON 的模板引擎,这里只是贪方便,你也可以直接把 JSON 结构写在动作的render
函数里面。
上面提到的分页是比较简单的,就是直接套用了Kaminari提供的工具方法,这些工具方法是跟模型(Model)以及 Rails 查询结果本身挂钩的
> User.class # 模型本身
=> Class
> User.page(1).class # 模型本身可以调用`page`方法
=> User::ActiveRecord_Relation
> User.page(1).per(2) # Rails的查询结果可以调用`per`方法
=> User::ActiveRecord_Relation
注意:page
跟per
方法都是 Kaminari 提供的。
这里为什么要强调 Rails 的查询结果呢?那是为了要跟数据库的查询结果做区分。其实在一些较为复杂的场景下,Rails 本身所提供的方法就不太够用了,这种时候则需要自己写一些复杂的 SQL。我们可以利用find_by_sql
方法又或者是ActiveRecord::Base.connection.execute
来执行相关的 SQL 语句
> User.find_by_sql('select * from users').class
=> Array
> User.all.class
=> User::ActiveRecord_Relation
> ActiveRecord::Base.connection.execute('select * from users').class
=> PG::Result
其实三者都是执行了类似的 SQL 语句,但是结果会封装到不同的数据结构里面。而 Kaminari 的工具方法只能应用在 Rails 的 ORM 模型(比如User
)或者查询结果中(比如User::ActiveRecord_Relation
),对其他两种并不生效
> User.find_by_sql('select * from users').page(1)
User Load (0.6ms) select * from users
Traceback (most recent call last):
1: from (irb):20
NoMethodError (undefined method `page' for #<Array:0x0000000009bd0278>)
> ActiveRecord::Base.connection.execute('select * from users').page(1)
(0.5ms) select * from users
Traceback (most recent call last):
2: from (irb):20
1: from (irb):21:in `rescue in irb_binding'
NoMethodError (undefined method `page' for #<PG::Result:0x0000000009e09648>)
如果我像针对另外两种查询结果进行分页那咋整呢?Kaminari 提供了比较方便的函数Kaminari.paginate_array,可以直接对数组来分页,所以针对上面的结果我们其实可以这样去分页
> results = Kaminari.paginate_array(User.find_by_sql('select * from users'))
User Load (0.5ms) select * from users
=> [#<User id: 48, nickname: "User#4", avatar: nil, mobile: "13744205053", openid: "0a18267e4917...
> results.page(1).per(2)
=> [#<User id: 48, nickname: "User#4", avatar: nil, mobile: "13744205053" ....
> results.page(2).per(2)
=> [#<User id: 47, nickname: "User#3", avatar: nil, mobile: "13744206757" ....
> results.total_count
=> 44
> results = Kaminari.paginate_array(ActiveRecord::Base.connection.execute('select * from users').to_a)
(0.7ms) select * from users
=> [{"id"=>48, "nickname"=>"User#4", "avatar"=>nil, "mobile"=>"13744205053", "openid"=>"0a18267e...
> results.page(1).per(2)
=> [{"id"=>48, "nickname"=>"User#4", "avatar"=>nil, "mobile"=>"13744205053", ...
> results.page(2).per(2)
=> [{"id"=>47, "nickname"=>"User#3", "avatar"=>nil, "mobile"=>"13744206757", ...
> results.total_count
=> 44
可见,只要简单地把PG::Result
的数据用to_a
转换成数组类型就能够进行分页。而且两者获得的结果集是类似的,最大的不同在于,通过User.find_by_sql
获得的结果是一个User
对象组成的数组。而通过ActiveRecord::Base.connection.execute
获得的结果是一个PG::Result
,经过to_a
处理之后得到的是Hash
对象组成的数组,他们包含的数据几乎一样但是就可操作性来讲还是User
对象更好一些
> result_from_hash = ActiveRecord::Base.connection.execute('select * from users').to_a.first # Hash对象
(0.5ms) select * from users
=> {"id"=>48, "nickname"=>"User#4", "avatar"=>nil, "mobile"=>"...
> result_from_hash.id
Traceback (most recent call last):
1: from (irb):3
NoMethodError (undefined method `id' for #<Hash:0x00000000032622a0>)
> result_from_hash['id']
=> 48
> result_from_user = User.find_by_sql('select * from users').first # User对象
User Load (0.6ms) select * from users
=> #<User id: 48, nickname: "User#4", avatar: nil, mobile: "13...
irb(main):006:0> result_from_user.id
=> 48
Hash
对象要获取字段数据得通过Hash#[]
方法,而User
对象直接能够以函数调用的模式xx.xx
去获取。
在一些较为复杂的场景下,Rails 的 ORM 提供的查询往往不太够用,在这种场景下要手写 SQL 语句(我这里为了方便举例所以采用了较为简单的查询,真实的业务会复杂许多)。而在这种情况下 Kaminari 提供的工具方法已经不能直接使用了,我们可以通过Kaminari.paginate_array来对查询结果进行分页。
然而这种方法有很大的问题它必须要把数据全部查询出来之后,再对结果进行分页。利用这种方式得到的结果也会拥有page
, per
, total_count
这些工具函数,代码的写法基本一致,这对于数据量不大或者程序员想偷懒的场景已然够用了。如果数据量到了一定的级别,这种做法就没法满足了,我们不太可能从数据库直接获取出 1000 甚至 2000 条数据,然后对这几千条数据进行分页,对于接口来说这种数据量的获取绝对会拖慢接口的响应速度。这个问题我们留在下一个篇章解决。
这篇文章简单介绍了一下 Rails 里面做分页的方式,利用工具库Kaminari并依赖 Rails 的 ORM,我们能够很简单地写出分页的代码。然而有些场景下我们不得不用原生的 SQL 语句来解决业务上的查询问题,这种场景下就无法直接依赖 Kaminari 提供的工具函数。我们后面使用它所提供的Kaminari.paginate_array
对结果进行封装,还是可以实现类似的分页效果的,然而这只是“权益之计”。从长远来看,要应对数据量巨大的场景,还是不得不借助数据库的分页机制,下一篇文章(进阶篇)将会详细谈谈面临复杂场景,不得不在 Rails 中手写 SQL 语句来查询的时候,如何利用数据库的分页机制,实现适应性更好的分页功能。