Rails 4, подзапросы ActiveRecord SQL

:parent has_many :children и я пытаюсь получить возраст самого старшего ребенка для родителя в качестве атрибута на parent, Я открыт для любого решения, которое достигает этого эффективно.

Причина, по которой я пытаюсь сделать подзапрос, состоит в том, чтобы позволить БД выполнить n+1 накладные расходы вместо создания отдельного запроса БД для каждого родителя. Оба неэффективны, но использование подзапроса кажется более эффективным.

# attributes: id
class Parent < ActiveRecord::Base
  has_many :children

  # Results in an (n+1) request
  def age_of_oldest_child
    children.maximum(:age)
  end
end

# attributes: id, parent_id, age
class Child < ActiveRecord::Base
  belongs_to :parent
end

Пример использования:

parent = Parent.first.age_of_oldest_child # => 16

parents = Parent.all
parents.each do |parent|
  puts parent.age_of_oldest_child # => 16, ...
end

Моя попытка:

sql = "
  SELECT 
    (SELECT
      MAX(children.age)
      FROM children
      WHERE children.parent_id = parents.id
    ) AS age_of_oldest_child
  FROM
    parents;
"

Parent.find_by_sql(sql)

Это возвращает массив максимальных возрастов для всех родителей; Я хотел бы ограничить это только одним родителем или также включить его в качестве атрибута для родителя при извлечении всех родителей.

Обновление 2015-06-19 11:00

Вот реальное решение, которое я придумал; Есть ли более эффективные альтернативы?

class Parent < ActiveRecord::Base
  scope :with_oldest_child, -> { includes(:oldest_child) }

  has_many :children
  has_one :oldest_child, -> { order(age: :desc).select(:age, :parent_id) }, class_name: Child

  def age_of_oldest_child
    oldest_child && oldest_child.age
  end
end

Пример использования:

# 2 DB queries, 1 for parent and 1 for oldest_child
parent = Parent.with_oldest_child.find(1)

# No further DB queries
parent.age_of_oldest_child # => 16

1 ответ

Решение

Вот два способа сделать это:

parent.rb

class Parent < ActiveRecord::Base
  has_many :children

  # Leaves choice of hitting DB up to Rails
  def age_of_oldest_child_1
    children.max_by(&:age)
  end

  # Always hits DB, but avoids instantiating Child objects
  def age_of_oldest_child_2
    Child.where(parent: self).maximum(:age)
  end
end

Первый метод использует перечислимый модуль max_by функциональность и звонки age на каждом объекте в коллекции. Преимущество этого заключается в том, что вы оставляете логику того, попадать ли в базу данных Rails или нет. Если children по какой-то причине уже создан, он больше не попадет в базу данных. Если они не созданы, он выполнит запрос на выборку, загрузит их в память за один запрос (таким образом, избегая N+1), а затем пройдет через каждый вызов его age метод.

Два недостатка, однако, заключаются в том, что если базовые данные изменились с момента создания дочерних объектов, они все равно будут использовать устаревший результат (этого можно избежать, передав :true при звонке :children, Кроме того, он загружает каждый child сначала в память, затем подсчитав их. Если child Объект большой и / или родитель имеет большое количество дочерних элементов, что может быть связано с большим объемом памяти. Это действительно зависит от вашего варианта использования.

Если вы решили, что хотите избежать загрузки всех этих children, вы можете делать прямой удар по БД каждый раз, используя count запрос изображен в методе 2. На самом деле, вы, вероятно, захотите переместить это в область видимости в Child поскольку, возможно, некоторые посчитают, что выполнение таких запросов вне целевой модели - это не шаблон, но это просто упрощает наглядность для примера.

Другие вопросы по тегам