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
поскольку, возможно, некоторые посчитают, что выполнение таких запросов вне целевой модели - это не шаблон, но это просто упрощает наглядность для примера.