Rails Group_By反模式

时间:2011-04-28 22:37:34

标签: ruby-on-rails activerecord group-by

当我使用Enumerable group_by方法时,我经常会遇到代码异味。我正在重构的一些旧代码就是一个很好的例子

def punctuality_report
  params[:date] ? @date = Date.strptime(params[:date], "%m/%d/%Y") : @date = Date.today
  params[:through] ? @through = Date.strptime(params[:through], "%m/%d/%Y") : @through = @date + 1
  time_range = @date.to_formatted_s(:db)..@through.to_formatted_s(:db)
  @orders = Order.daily.includes(:store).where('orders.created_at' => time_range).group_by{|o| o.store}
  @orders.each_key.each do |s|
    eval "@s#{s.id}_early = @orders[s].collect{|o| o if o.early?}.compact"
    eval "@s#{s.id}_avg_early = @s#{s.id}_early.count > 0 ? @s#{s.id}_early.collect{|o| o.earliness}.sum / @s#{s.id}_early.count : '0'"         
    eval "@s#{s.id}_late = @orders[s].collect{|o| o if o.late?}.compact"
    eval "@s#{s.id}_avg_late =  @s#{s.id}_late.count > 0 ? @s#{s.id}_late.collect{|o| o.lateness}.sum / @s#{s.id}_late.count : '0'"         
    eval "@s#{s.id}_on_time = @orders[s] - (@s#{s.id}_early | @s#{s.id}_late)"
  end
end

好的,所以我正在解决这个问题,我清楚地看到我们需要将这个报告从订单控制器上的一个动作重构为一个自己的模型来清理这个实现逻辑。有一个代码味道,但我仍然很难使用这个orders.group_by哈希。

事情是,当我在视图层时,我真的想要那个哈希。我需要从订单中获取摘要,但我需要访问商店。在Activerecord中使用组查询方法只返回一个关系,它不像可枚举的group_by哈希那样有用。我觉得有更好的方法来获得我需要的东西并减少大量的查询和红宝石处理。

3 个答案:

答案 0 :(得分:2)

我真的没有看到group_by方法有什么问题。我真的看到过度使用eval的问题[ - ; Eval比group_by

具有更强的代码味道(恕我直言)

话虽如此,我确实看到了重构的其他一些方面:

# Consider this refactor:

def punctuality_report
  # I find this slightly more readable
  @date = (params[:date]) ? Date.parse(params[:date]) : Date.today
  @through = (params[:through]) ? Date.parse(params[:through]) : @date + 1.day

  # the .in_time_range(range) method can be defined as a scope on the Order model, and you can
  # get rid of that logic here
  # 
  @orders = Order.daily.includes(:store).in_time_range(@date, @through).group_by(&:store)

end

class Order < ActiveRecord::Base

  scope :in_time_range, lambda { |date, through| where('orders.created_at' => (date..through)) }

  # It looks like you already know how to collect the orders for your needs... ie Order#early, etc

end

# Now, consider in your views the ability to do this:
@orders.each do |store, orders|
  # the orders now are the orders that met the above qualifications for the store
  orders.early
  Order.average_of_orders(orders.early)
  orders.late
  Order.average_of_orders(orders.late)
  orders.on_time    
end

答案 1 :(得分:1)

我认为如果使用得当,group_by是一个非常有效的工具。我在一个cron作业中有一个方法,它使用了几个嵌套的group_by语句,我确信这会导致显着的减速。我花了2个小时重写方法来摆脱group_by,运行时间从1小时8分钟变为1小时5分钟。非常值得努力。

另一方面,该代码存在一些严重问题。那些三元运算符实际上让我大笑,而eval语句只是邪恶的。他们不仅使用了eval,而且使用得很厉害。调用eval非常慢,并且没有理由不能将所有5个eval语句合并为一个。

那就是说,即使这个方法中的一个eval语句太多了。我确定eval有时间和地点,但我的代码中从未需要它。根据我的一般经验,几乎所有对eval的调用都可以用send替换,这样更快更安全。

答案 2 :(得分:1)

我的尝试:

def punctuality_report
  @date    = params[:date]    ? Date.strptime(params[:date], "%m/%d/%Y")    : Date.today
  @through = params[:through] ? Date.strptime(params[:through], "%m/%d/%Y") : @date + 1

  time_range = @date.to_formatted_s(:db)..@through.to_formatted_s(:db)

  @orders = Order.daily.includes(:store).where('orders.created_at' => time_range).group_by{|o| o.store}

  @orders.each_key.each do |s|
    all_early = @orders[s].collect{|o| o if o.early?}.compact
    all_late  = @orders[s].collect{|o| o if o.late?}.compact
    self.instance_variable_set("@s#{s.id}_early", all_early)   
    self.instance_variable_set("@s#{s.id}_avg_early", all_early.count > 0 ? all_early.collect{|o| o.earliness}.sum / all_early.count : 0)         
    self.instance_variable_set("@s#{s.id}_late", all_late) 
    self.instance_variable_set("@s#{s.id}_avg_late", all_late.count > 0 ? all_late.collect{|o| o.lateness}.sum / all_late.count : 0)         
    self.instance_variable_set("@s#{s.id}_on_time", @orders[s] - (all_early | all_late) )
  end
end

简而言之:在eval部分中,只添加了实例变量,我们可以使用instance_variable_getinstance_variable_set代替。几乎所有eval的出现都可以避免,但我也相信eval有时非常有用而且非常强大。

代码更清楚地表达了它的意图:它将添加一组实例变量,这些变量立即可见。

我绝对不会考虑使用group_by作为代码气味。