Rails 小技巧(一) - 列表数据的查询和汇总

环境(Environment)

  • Ruby: 2.7.2
  • Rails: 6.1.0
  • OS: Manjaro i3 20.2.1

场景(Scenario)

Index控制器里边获取列表几乎是最常见的需求了,一般获取数据的时候,还会附加筛选条件,返回的结果数据也需要包含如total_count之类的汇总信息。下面介绍一种我比较喜欢的方式来实现这样的需求。


筛选(Filter)

一般来说筛选会包含多个条件,条件不一定都有值,所以可能会出现多种条件组合的情况。

这里使用scope来实现该功能,这里扒一段Railsguide 上面的内容:

我们可以在作用域中使用条件:

class Article < ApplicationRecord
  scope :created_before, ->(time) do
      where("created_at < ?", time) if time.present?
  end
end

和之前的例子一样,作用域的这一行为也和类方法类似。

class Article < ApplicationRecord
  def self.created_before(time)
    where("created_at < ?", time) if time.present?
  end
end

不过有一点需要特别注意:不管条件的值是 true 还是 false,作用域总是返回ActiveRecord::Relation对象,而当条件是 false 时,类方法返回的是 nil。因此,当链接带有条件的类方法时,如果任何一个条件的值是 false,就会引发 NoMethodError 异常。

利用scope默认会返回ActiveRecord::Relation的特性,我们可以将参数判断放在scope里边,当参数不满足条件时,里边的筛选将不会执行,同时返回一个基于已有筛选条件的关联,我们紧接着这个关联继续调用,这样就可以写出很优雅的查询语句。For example:

# controller
class ArticlesController < ApplicationController
  def index
    # 即使某个参数为空,语句也能正常执行
    @articles = Article.created_before(params[:created_before])
                 .created_after(params[:created_after])
                 .by_state(params[:state])
  end
end

# model 
class Article < ApplicationRecord
    scope :created_before, ->(time) do 
        where('created_at < ?', time.in_time_zone) if time.present?
    end

    scope :created_after, ->(time) do 
        where('created_at > ?', time.in_time_zone) if time.present?
    end

    scope :by_state, ->(state) do 
        where(state: state) if state.present?
    end
end

汇总数据(Statistics)

如果使用了Kaminari之类的分页Gem,并且在分页前需要获取某些条件的汇总数据。比如说:我们现在需要查出2021-03-02 00:00:002021-03-05 00:00:00之间的文章(分页),同时还要得到每个状态(draft,public,deleted)下的总数,那么可以这样子写:

# controller
class ArticlesController < ApplicationController
  def index
    # 即使某个参数为空,语句也能正常执行
    @posts = Article.created_before(params[:created_before])
                 .created_after(params[:created_after])
                 .by_state(params[:state])
                 .tap do |records|
                    result = records.group(:state).count
                    @statistic = {
                        draft: 0,
                        public: 0,
                        deleted: 0
                    }.merge(result.symbolize_keys)
                 end
                 .page(params[:page] || 1) # 分页假设使用 kaminari
                 .per(params[:per] || 25)
  end
end

# model 
class Article < ApplicationRecord
    scope :created_before, ->(time) do 
        where('created_at < ?', time.in_time_zone) if time.present?
    end

    scope :created_after, ->(time) do 
        where('created_at > ?', time.in_time_zone) if time.present?
    end

    scope :by_state, ->(state) do 
        where(state: state) if state.present?
    end
end

这里用到了tap这个方法,这个会把作为参数传入块中,然后再返回自己。

# 这段扒自 https://ruby-china.org/topics/5348

def tap     #tap 源码实现
  yield self
  self
end

写在最后

最後までご覧いただいてありがとうございます~


Rails 小技巧系列

点赞

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注