提高性能:避免在集合中查找正确的元素

时间:2017-06-15 15:33:38

标签: ruby-on-rails ruby

我有这种情况。

activity.rb

belongs_to :user
belongs_to :cause
belongs_to :sub_cause
belongs_to :client

def amount
  duration / 60.0 * user.hourly_cost_by_year(date.year).amount rescue 0
end

user.rb

has_many :hourly_costs # one hourly_cost for year
has_many :activities

def hourly_cost_by_year(year = Date.today.year)
  hourly_costs.find { |hc| hc.year == year }
end

hourly_cost.rb

belongs_to :user

我有一个很好的报告,我取得了良好的性能(SQL查询的数量是固定的)但我认为我可以做得更好。我使用的查询是

activities = Activity.includes(:client, :cause, :sub_cause, user: :hourly_costs)

这没关系,它很快,但我认为是可以改进的,因为hourly_cost_by_year方法。我的意思是,活动有一个日期,我可以使用该日期来了解我应该使用的每小时费用。 activity

中有类似的内容
def self.user_with_single_hourly_cost
  joins('LEFT JOIN users u ON u.id = activities.user_id').
  joins('LEFT JOIN hourly_costs hc ON hc.user_id = u.id AND hc.year = EXTRACT(year from activities.date)')
end

但我不知道如何将它整合到我的查询中。无论我尝试什么都行不通。我可以使用原始SQL,但我正在尝试使用ActiveRecord。我甚至认为使用redis按用户和年份缓存每小时的成本,可以工作,但我认为这个查询与提取部分应该做得最好,因为我有一个平面表。

更新:我试着澄清一下。无论我在行动中使用什么查询,我都必须这样做

activities.sum(&:amount)

,那个方法,你知道,是

def amount
  duration / 60.0 * user.hourly_cost_by_year(date.year).amount rescue 0
end

而且我不知道如何在hourly_costs之间直接选择我想要的hourly_cost而不进行搜索。这可能吗?

1 个答案:

答案 0 :(得分:2)

您可以考虑使用ArelArel是rails / activerecord的基础查询汇编程序(因此没有新的依赖项),并且在构建复杂查询时非常有用,因为它提供的深度远远高于高级ActiveRecord::QueryMethods

显然,使用更广泛的API会带来更多的冗长(这实际上会增加可读性)和较少的语法糖,这需要一些人习惯,但已经证明在多个场合对我来说是不可或缺的。

虽然我没有花时间重新创建您的数据结构,但这可能对您有用

activities = Activity.arel_table
users = User.arel_table
hourly_costs = HourlyCost.arel_table

activity_users_hourly_cost = activities
  .join(users,Arel::Nodes::OuterJoin)
    .on(activities[:user_id].eq(users[:id]))
  .join(hourly_costs,Arel::Nodes::OuterJoin)
    .on(hourly_costs[:user_id].eq(users[:id])
      .and(hourly_costs[:year].eq(Arel::Nodes::Extract.new(activities[:date],'year'))
    )
  )
Activity.includes(:client, :cause, :sub_cause).joins(activity_users_hourly_cost.join_sources)

这将添加请求的加入,例如

activity_users_hourly_cost.to_sql 
#=> SELECT 
    FROM [activities] 
    LEFT OUTER JOIN [users] ON [activities].[user_id] = [users].[id] 
    LEFT OUTER JOIN [hourly_costs] ON [hourly_costs].[user_id] = [users].[id] 
        AND [hourly_costs].[year] = EXTRACT(YEAR FROM [activities].[date])

<强>更新

如果您只想添加&#34; hourly_cost&#34;这应该对你有用

  Activity.includes(:client, :cause, :sub_cause)
    .joins(activity_users_hourly_cost.join_sources)
    .select("activities.*, activities.duration / 60.0 * ISNULL([hourly_costs].[amount],0) as hourly_cost_by_year") 

请注意,这只会返回Activity个对象,但现在会有一个名为hourly_cost_by_year的方法,它将返回该计算的结果。完整SQL看起来像

   SELECT 
     [activities].*,
     activities.duration / 60.0 * ISNULL([hourly_costs].[amount],0) as hourly_cost_by_year
    FROM [activities]
    -- Dependant upon WHERE Clause 
     LEFT OUTER JOIN causes ON [activities].[cause_id] = [causes].[id]
     LEFT OUTER JOIN sub_causes ON [activities].[subcause_id] = [subcauses].[id]
     LEFT OUTER JOIN clients [activities].[client_id] = [clients].[id]
    --
    LEFT OUTER JOIN [users] ON [activities].[user_id] = [users].[id] 
    LEFT OUTER JOIN [hourly_costs] ON [hourly_costs].[user_id] = [users].[id] 
        AND [hourly_costs].[year] = EXTRACT(YEAR FROM [activities].[date])

如果你愿意,你也可以在Arel中构建选择部分,但对于这样一个简单的陈述来说似乎有些过分。