Rails:当我们需要在循环内查询关联时,如何删除n + 1查询?

时间:2019-05-02 07:37:51

标签: ruby-on-rails ruby ruby-on-rails-4

我在其中有查询的代码中输出了结果(此处仅显示基本内容) 因此,基本上,我需要自定义订单项以及所有订单项的总和

results = Order.includes(:customer, :line_items).where('completed_at IS NOT NULL')

results.each do |result|
  custom_items_sum = result.line_items.where(line_item_type: 'custom').sum(:amount)
   total_sum = result.line_items.sum(:amount)
end

在此代码中,存在n + 1个查询问题,我尝试添加包含,但由于在循环中有另一个查询,因此确保它无法正常工作,我们将不胜感激。

4 个答案:

答案 0 :(得分:3)

如果您不想在循环中触发其他查询,则需要避免对关系起作用的方法,而应使用对集合起作用的方法。试试

custom_items_sum = result.line_items.
    select { |line_item| line_item.line_item_type == 'custom' }.
    sum(&:amount)

这应该在没有n + 1个查询的情况下起作用。

请注意,有可能只编写一个查询,无论如何避免这种计算,但这超出了您的问题范围:)

答案 1 :(得分:2)

Rails从未像ORM那样强大。改用普通SQL:

results =
  Order.connection.execute <<-SQL
    SELECT order.id, SUM(line_items.amount)
    FROM orders
      JOIN line_items
      ON (line_items.order_id = orders.id)
    WHERE orders.completed_at IS NOT NULL
    GROUP BY orders.id
    HAVING line_items.line_item_type = 'custom'
  SQL

这样,您将在一个查询中获得所有中间和,这比在ruby中执行所有计算要快得多。

答案 2 :(得分:1)

尝试使用作用域块。以下代码生成非常干净的SQL查询。

Order.includes(:line_items).where.not(completed_at: nil).scoping do
   @custom_items_sum = Order.where(line_items: { line_item_type: 'custom' })
                            .sum(:amount)
   @total_sum        = Order.sum(:amount)
end

关于scoping块的文档并不多,但是它将您的模型的范围限定为之前发出的ActiveRecord请求(此处为where('completed IS NOT NULL'),并附带了:line_items)。

希望这会有所帮助! :)

答案 3 :(得分:1)

只是因为@AlekseiMatiushkin说用原始SQL编写它,所以我们对rails做同样的事情

order_table = Order.arel_table
line_items_table = LineItem.arel_table
custom_items = Arel::Table.new(:custom_items)
Order.select(
   order_table[Arel.star],
   line_items_table[:amount].sum.as('total_sum'),
   custom_items[:amount].sum.as('custom_items_sum')
).joins(
   order_table.join(line_items_table).on(
     line_items_table[:order_id].eq(order_table[:id])
   ).join(
      Arel::Nodes::As.new(line_items_table,:custom_items), 
      Arel::Nodes::OuterJoin
   ).on( 
      custom_items[:order_id].eq(order_table[:id]).and(
       custom_items[:line_item_type].eq('custom')
      ) 
   ).join_sources
).where(
   order_table[:completed_at].not_eq(nil)
).group(:id)

这将使用以下查询生成ActiveRecord::Relation个对象,其中Order个对象的虚拟属性分别为total_sumcustom_items_sum

SELECT 
  orders.*,
  SUM(line_items.amount) AS total_sum,
  SUM(custom_items.amount) As custom_items_sum
FROM 
  orders
  INNER JOIN line_items ON line_items.order_id = orders.id
  LEFT OUTER JOIN line_items AS custom_items ON custom_items.order_id = orders.id
    AND custom_items.line_item_type = 'custom'
WHERE 
  orders.completed_at IS NOT NULL
GROUP BY 
  orders.id

这应该通过使用2个联接来聚合所需的数据,从而在单个查询中处理请求。