通常来说,分页通过 offset
和 limit
来实现,借助第三方 gem 我们可以这样写:collection.page(params[:page]).per_page(params[:per_page]).padding(params[:offset])
.
但是,当列表数据会频繁变动时 (eg, 论坛里热门话题的回复列表,正在进行的游戏列表 ), 传统分页就会出现前后两页数据重复的问题,基于游标的分页就上场啦。基于游标的分页,就是指明上一页最后一项的 ID
, 取该项之后的 N
项。
所谓简单场景,就是列表没有复杂的排序,按ID
或者 created_at
来排序。这个问题可以这样来解决:
collection.where('id > ?', params[:last_id]).limit(params[:limit])
真实项目中,大多数列表的排序都很复杂,例如正在进行的游戏列表,很可能要根据权重,开始时间和热门程度进行组合排序,这种情况下的解决方案就没那么美了。
第一种思路是利用MySQL
的自增虚拟列,伪代码是这样的:
select * from
(select distinct (@i:=@i+1) as i, games.* from games , ( select @i:=0 ) as it order by games.updated_at, games.score, games.popular ) as ordered_columns where ordered_columns.i >
(select ordered_columns.i from
(select distinct (@i:=@i+1) as i, games.* from games , ( select @i:=0 ) as it where games.id = 10 order by games.updated_at, games.score, games.popular ) as ordered_columns);
代码中的 @user2
@user5
都是 @i
, 搞不懂这是啥情况,呼唤 @huacnlee
这样的代码很明显太丑了,也很难跟ArctiveRecord
配合使用,或许可以用 Arel
?
第二种思路适用于数据量较少的情景。我直接贴 (简化) 代码了:
ids = collection.ids
start_index = ids.index(params[:last_id])
collection.first ? collection.first.class.find_ordered(ids[start_index, params[:limit]]) : collection
MySQL
有个小坑,select * from games where id in (7, 1, 10)
查到的数据排序,跟你想的不太一样,你可以试一下。这里monkey patch
了下:
module Extensions::ActiveRecord::FindByOrderedIds
extend ActiveSupport::Concern
module ClassMethods
def find_ordered(ids)
sanitized_id_string = ids.map {|id| connection.quote(id)}.join(",")
where(id: ids).order("FIELD(id, #{sanitized_id_string})")
end
end
end
ActiveRecord::Base.include(Extensions::ActiveRecord::FindByOrderedIds)
暂时只想到这两种思路,论坛里的各位大神,大家是如何解决类似问题的?