Rails: Как удалить n + 1 запрос, когда нам нужно запросить ассоциацию внутри цикла? - PullRequest
1 голос
/ 02 мая 2019

В результате я получаю вывод, что в коде есть запросы (здесь показаны только основные). Поэтому в основном мне нужна сумма пользовательских позиций, а также всех позиций

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 ]

3 голосов
/ 02 мая 2019

Если вы не хотите запускать другие запросы в цикле, вам нужно избегать методов, которые работают с отношениями, и использовать те, которые работают с коллекциями.Попробуйте

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

Это должно работать без n + 1 запросов.

Обратите внимание, что можно написать только один запрос и в любом случае избежать этого вычисления, но это выходит за рамки вашего вопроса:)

2 голосов
/ 02 мая 2019

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.

1 голос
/ 02 мая 2019

Только потому, что @ 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_sum и custom_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 объединения для объединения необходимых данных.

1 голос
/ 02 мая 2019

Попробуйте использовать блок обзора. Следующий код генерирует очень чистые запросы 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).

Надеюсь, это поможет! :)

...