1. 介绍
假如我们有一个博客网站,有很多文章,用户在查看一篇文章后,在文章尾部可以查看前一篇和后一篇文章。这种需求很容易完成,一般来说,id比当前文章的id大的最近的一条记录就是后一篇文章,只需要一条where("id > ?")语句就可以解决。
但有时候没有这么简单。比如,用户在文章列表通过搜索或排序之后,进入文章,它的下一篇也要按照之前文章列表排序后的顺序出现。也就是用id来比较是无效的。
而order_query就是来解决这一问题的,它会按照自己写好的条件来取出结果,再来排序。排好后,把你的记录传过去,就可以返回这条记录前的所有记录和后面的所有录,还有位置等信息,它就是提供了比较好的接口,让你的代码更加简洁。特别适合于瀑布流的应用。
2. 安装
添加下面一行到Gemfile文件。
gem 'order_query', '~> 0.3.2'
执行bundle
。
3. 使用
以本站为例,所有的博客文章是存在articles表中的,visit_count是article的浏览量,现在我们要按照visit_count来排序。
class Article < ActiveRecord::Base
include OrderQuery
order_query :order_title,
[:visit_count, :desc],
[:id, :desc]
end
order_query方法有点类似于scope,第一个是定义排序方法的名称,第二个是排序的数组,先按visit_count从大到小排,再按id从大到小来排。
进入rails console
进行测试。
首先我们先来看看通过order_title排序后的数据是如何的。
Article.order_title.map {|article| "#{article.title} => {visit_count: #{article.visit_count}}, {id: #{article.id}}"}
排序产生的sql语句是这样的。
SELECT "articles".* FROM "articles" ORDER BY "articles"."visit_count" DESC, "articles"."id" DESC
结果记录是下面这样。
[
[ 0] "登录认证系统的进阶使用 => {visit_count: 124}, {id: 4}",
[ 1] "devise简单入门教程 => {visit_count: 123}, {id: 3}",
[ 2] "在阿里云ubuntu主机上安装ruby on rails部署环境 => {visit_count: 88}, {id: 5}",
[ 3] "使用Monit来监控服务 => {visit_count: 62}, {id: 11}",
[ 4] "使用mina来部署ruby on rails应用 => {visit_count: 62}, {id: 7}",
[ 5] "用OneAPM作为你的监控平台 => {visit_count: 56}, {id: 8}",
[ 6] "升级centos系统上的nginx => {visit_count: 44}, {id: 6}",
[ 7] "用exception_notification结合Slack或数据库来捕获异常 => {visit_count: 42}, {id: 13}",
[ 8] "Mina的进阶使用 => {visit_count: 35}, {id: 12}",
[ 9] "用logrotate切割Ruby on rails日志 => {visit_count: 34}, {id: 10}",
[10] "使用backup来备份数据库 => {visit_count: 31}, {id: 9}"
]
下面我们以一个实例来说明order_query的用法。我们找上面的第六条记录,也即id等于8的那条"用OneAPM作为你的监控平台",我们来找出它的上一条记录。
article = Article.find 8
Article.order_title_at(article).previous.title
使用的sql语句为:
SELECT "articles".* FROM "articles" WHERE ("articles"."visit_count" >= 56 AND ("articles"."visit_count" > 56 OR "articles"."visit_count" = 56 AND "articles"."id" > 8)) ORDER BY "articles"."visit_count" ASC, "articles"."id" ASC LIMIT 1
结果正是我们期望的。
"使用mina来部署ruby on rails应用"
假如要返回article前面的所有记录,只需要使用'before'就好了,比如:
Article.order_title_at(article).before
order_query还支持动态的列查找,也就是说,未必要在model文件里事先定义,可以这样。
Article.seek([:visit_count, :desc])
关于其他更多的用法,只要查看官方readme文档就好。
4. 源码解析
接下来是分析order_query的源码实现,我们从model中类方法order_query入手。
# https://github.com/glebm/order_query/blob/master/lib/order_query.rb#L61
def order_query(name, *spec)
define_singleton_method(:"#{name}_space") { seek(*spec) }
class_eval <<-RUBY, __FILE__, __LINE__
scope :#{name}, -> { #{name}_space.scope }
scope :#{name}_reverse, -> { #{name}_space.scope_reverse }
def self.#{name}_at(record)
#{name}_space.at(record)
end
def #{name}(scope = self.class)
scope.#{name}_space.at(self)
end
RUBY
end
order_query
的源码很简单,只是定义了几个方法。先来看这个scope :#{name}, -> { #{name}_space.scope }
,#{name}_space
这个方法是这行define_singleton_method(:"#{name}_space") { seek(*spec) }
定义的,其实最终就是seek(*spec)
这个方法,只要能理解seek
这个方法,就等于破解了整个order_query
方法。
# https://github.com/glebm/order_query/blob/master/lib/order_query.rb#L27
def seek(*spec)
# allow passing without a splat, as we can easily distinguish
spec = spec.first if spec.length == 1 && spec.first.first.is_a?(Array)
Space.new(all, spec)
end
主要是Space.new(all, spec)
这个方法。
# https://github.com/glebm/order_query/blob/master/lib/order_query/space.rb#L12
def initialize(base_scope, order_spec)
@base_scope = base_scope
@columns = order_spec.map { |cond_spec| Column.new(cond_spec, base_scope) }
# add primary key if columns are not unique
unless @columns.last.unique?
raise ArgumentError.new('Unique column must be last') if @columns.detect(&:unique?)
@columns << Column.new([base_scope.primary_key], base_scope)
end
@order_by_sql = SQL::OrderBy.new(@columns)
end
会把那排序的列,比如[:visit_count, :desc], [:id, :desc]
给传到@columns = order_spec.map { |cond_spec| Column.new(cond_spec, base_scope) }
这行来,处理完之后把结果作为参数传给@order_by_sql = SQL::OrderBy.new(@columns)
这一行,最后存到@order_by_sql变量中。
我们先来看一个例子Article.seek([:visit_count, :desc]).first
返回的是排序后的第一个元素。
# https://github.com/glebm/order_query/blob/master/lib/order_query/space.rb#L29
def scope
@scope ||= @base_scope.order(@order_by_sql.build)
end
# https://github.com/glebm/order_query/blob/master/lib/order_query/space.rb#L39
# @return [ActiveRecord::Base]
def first
scope.first
end
看到这一部分@order_by_sql.build
就是利用了上面的结果。那就来看@order_by_sql.build
到底做了什么。
# https://github.com/glebm/order_query/blob/master/lib/order_query/sql/order_by.rb#L3
class OrderBy
# @param [Array<Column>]
def initialize(columns)
@columns = columns
end
# @return [String]
def build
@sql ||= join_order_by_clauses order_by_sql_clauses
end
# @return [String]
def build_reverse
@reverse_sql ||= join_order_by_clauses order_by_sql_clauses(true)
end
protected
# @return [Array<String>]
def order_by_sql_clauses(reverse = false)
@columns.map { |col| column_clause col, reverse }
end
def column_clause(col, reverse = false)
if col.order_enum
column_clause_enum col, reverse
else
column_clause_ray col, reverse
end
end
def column_clause_ray(col, reverse = false)
"#{col.column_name} #{sort_direction_sql(col, reverse)}".freeze
end
def column_clause_enum(col, reverse = false)
enum = col.order_enum
# Collapse boolean enum to `ORDER BY column ASC|DESC`
if enum == [false, true] || enum == [true, false]
return column_clause_ray col, reverse ^ enum.last
end
enum.map { |v|
"#{order_by_value_sql col, v} #{sort_direction_sql(col, reverse)}"
}.join(', ').freeze
end
def order_by_value_sql(col, v)
"#{col.column_name}=#{col.quote v}"
end
# @return [String]
def sort_direction_sql(col, reverse = false)
col.direction(reverse).to_s.upcase.freeze
end
# @param [Array<String>] clauses
def join_order_by_clauses(clauses)
clauses.join(', ').freeze
end
end
只要看仔细每个方法,就会发现最终还是生成了排序用的sql语句,比如"visit_count DESC, id DESC",这个可以传给order方法。那Article.seek([:visit_count, :desc]).first
就好理解了,自然跟Article.order("visit_count DESC, id DESC").first
一样的。
完结。
网友评论