Как удалить N + 1 запросов из моей ассоциации Rails? - PullRequest
0 голосов
/ 08 декабря 2018

В моем приложении 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 голосов
/ 08 декабря 2018

Во-первых, давайте перепишем этот метод 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')

Это идея.Я делал это много раз, но это действительно сложно, не пытаясь и не имея всей информации.Я думаю, что вы можете исправить это в случае.Если вы используете рельсы 5, вы можете использовать left_joins

0 голосов
/ 08 декабря 2018

Ответ от ursus должен быть правильным, но я хотел бы предоставить некоторую дополнительную информацию о коде, который вы написали.

select, который вы делаете в своем методе self.range, - это метод Enumerable или Array.который работает с запросом после его выполнения, и это одна из причин, по которой вы видите столько запросов. Enumerable # select

A select в ActiveRecord фактически выбирает, какие столбцы вы хотите получить:

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)

Это работает только на одного человека (что все еще может быть полезно).

Если вы хотите выполнить этот тип запроса к коллекции, вам понадобится что-то более сложное, как указывал Урсус.

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 метод, как заметил Урсус.

0 голосов
/ 08 декабря 2018

Попробуйте:

@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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...