美文网首页
ruby gem 介绍及源码解析系列之 order_query

ruby gem 介绍及源码解析系列之 order_query

作者: 求知久久编程学院 | 来源:发表于2020-01-16 14:45 被阅读0次

    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一样的。

    完结。

    相关文章

      网友评论

          本文标题:ruby gem 介绍及源码解析系列之 order_query

          本文链接:https://www.haomeiwen.com/subject/ycvfzctx.html