如何从我的Rails关联中删除N + 1个查询?

时间:2018-12-08 10:19:27

标签: ruby-on-rails ruby activerecord

在我的Rails应用程序中,我具有以下关联:

class Person < ApplicationRecord

  has_many :payments

  def sales_volume(range)
    payments.in_range(range).sum(&:amount)
  end

  ...

end

class Payment < ApplicationRecord

  belongs_to :person

  def self.in_range(range)
    select { |x| range.cover?(x.date) }
  end

  ...

end

在我的控制器中,我正在做...

@people = Person.all.sort_by { |p| p.sales_volume(@range) }

...而且有效。

但是,不幸的是,它会生成很多 N + 1个查询

有没有一种方法可以显示每个person的销量,而无需为每个人生成单独的SQL查询?


添加:

我已经尝试过快速加载...

@people = Person.all.includes(:payments).sort_by { |p| p.sales_volume(@range) }

...但是那也不起作用。查询数量仍然相同。

3 个答案:

答案 0 :(得分:5)

首先,让我们在in_range中重写该ActiveRecord方法

def self.in_range(range)
  where(date: range)
end

现在,那个sales_volume。假设

def self.with_sales_volume(range)
  joins("LEFT JOIN payments ON payments.user_id = users.id").
    merge(Payment.in_range(range)).
    group('users.id').
    select('users.*, SUM(payments.amount) AS sales_volume')
end

最后

@people = Person.from(Person.with_sales_volume(@range), :t).order('t.sales_volume DESC')

这是主意。我已经做过很多次了,但是如果不尝试并获取所有信息,这真的很难。我认为您可以解决此问题。如果您使用的是Rails 5,则可以使用left_joins

答案 1 :(得分:4)

ursus的答案应该是正确的,但我想提供有关您编写的代码的其他信息。

您在select方法中执行的self.range是一个Enumerable或Array方法,该方法在查询运行后对其进行操作,这是您看到如此多查询的原因之一。 Enumerable#select

ActiveRecord中的select实际上选择了要检索的列:

Person.select(:id, :name) # only returns the columns id & name

您要执行where

class Payment < ApplicationRecord
  belongs_to :person

  def self.in_range(range)
    where('date between ? and ?', range.begin, range.end)
  end
end

这使您可以执行以下操作:

Person.first.sales_volume(Time.zone.today - 30.days..Time.zone.today)

这仅适用于单个人(这可能仍然有用)。

如果您想对集合执行这种类型的查询,您将需要一些更复杂的操作,如ursus指出。

people =
  Person.
    select('people.*, SUM(payments.amount) AS sales_volume').
    joins(:payments).
    merge(
      Payment.
        in_range(
          (Time.zone.today - 30.days)..Time.zone.today)
        ).
    group('people.id')

people.first.sales_amount # prints sales amount

如您所见,您可以将所有这些转换为您的with_sales_volume方法。

答案 2 :(得分:1)

尝试一下:

@people = Person.all.includes(:payments).where(payments: { date: range }).sort_by { |p| p.payments.sum(&:amount) }

这个想法是将范围条件直接放入渴望的负载关联中,并更改求和的方式,以避免再次运行查询。

https://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations