Rails4、ActiveRecordSQL子查询

Rails 4, ActiveRecord SQL subqueries

A :parent has_many :children 并且我正在尝试检索 parent 的最老 child 的年龄作为 parent 的属性。我对任何能有效实现这一点的解决方案持开放态度。

我尝试执行子查询的原因是让数据库执行 n+1 开销,而不是为每个 parent 发出单独的数据库请求。两者都是低效的,但使用子查询似乎更有效。

# 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)

这个 returns 所有 parent 的最大年龄数组;我想将其限制为仅 1 parent 或者当我检索所有 parent 时也将其作为属性包含在 parent 中。

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

这里有两种方法:

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 由于某种原因已经实例化,它不会再次访问数据库。如果它们没有被实例化,它将执行一个 select 查询,在单个查询中将它们加载到内存中(从而避免 N+1),然后通过每个调用它的 age 方法。

然而,两个缺点是,如果基础数据在子实例化后发生变化,它仍将使用过时的结果(这可以通过在调用 [=16= 时传递 :true 来避免]。此外,它首先将每个 child 加载到内存中,然后对它们进行计数。如果 child 对象很大 and/or 父对象有大量子对象,那可能是内存-密集型。这实际上取决于您的用例。

如果您决定要避免加载所有这些 children,您可以每次使用方法 2 中描述的 count 查询直接访问数据库。事实上,您可能会实际上想将其重新定位到 Child 中的范围,因为也许有些人会认为在目标模型之外进行类似查询的反模式,但这只是让示例更容易看到。