简体   繁体   English

Rails 4,ActiveRecord SQL子查询

[英]Rails 4, ActiveRecord SQL subqueries

A :parent has_many :children and I am trying to retrieve the age of the oldest child for a parent as an attribute on the parent . :parent has_many :children ,我试图找回最古老的儿童的年龄为父母为上的某一属性parent I'm open to any solution that accomplishes that efficiently. 我愿意接受任何能有效实现这一目标的解决方案。

The reason I'm trying to do a subquery is to let the DB do the n+1 overhead instead of making a separate DB request for each parent. 我尝试执行子查询的原因是让数据库执行n+1开销,而不是为每个父级发出单独的数据库请求。 Both are inefficient, but using a subquery seems more efficient. 两者均效率低下,但使用子查询似乎更有效。

# 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

Sample use case: 示例用例:

parent = Parent.first.age_of_oldest_child # => 16

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

My attempt: 我的尝试:

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)

This returns an array of maximum ages for all parents; 这将返回所有父母的最大年龄数组。 I would like to restrict this to just 1 parent or also have it included as an attribute on a parent when I retrieve all parents. 我想将其限制为仅1个父级,或者在检索所有父级时也将其作为属性包含在父级中。

Update 2015-06-19 11:00 更新2015-06-19 11:00

Here is a workable solution I came up with; 这是我想出的可行解决方案; are there more efficient alternatives? 有更有效的替代方法吗?

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

Example usage: 用法示例:

# 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

Here are two ways of doing it: 这有两种方法:

parent.rb 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

The first method uses the enumerable module's max_by functionality and calls age on each object in the collection. 第一种方法使用可枚举模块的max_by功能,并在集合中的每个对象上调用age The advantage of doing it this way is you leave the logic of whether or not to hit the database to Rails. 这样做的好处是,您可以将是否命中数据库的逻辑留给Rails。 If the children are already instantiated for some reason, it won't hit the database again. 如果由于某种原因已经实例化了children ,它将不会再次访问数据库。 If they are not instantiated, it will perform a select query, load them into memory in a single query (thus avoiding N+1) and then go through each one calling its age method. 如果未实例化它们,它将执行选择查询,并在单个查询中将它们加载到内存中(从而避免N + 1),然后逐个调用其age方法。

The two disadvantages, however, are that if the underlying data has changed since the children were instantiated, it will still use the outdated result (this could be avoided by passing :true when calling :children . Also, it is loading every single child into memory first, then counting them. If the child object is large and/or a parent has a large number of children, that could be memory-intensive. It really depends on your use case. 但是,两个缺点是,如果自从实例化子代以来基础数据发生了变化,它仍将使用过时的结果(可以通过在调用:children时传递:true来避免。此外,它将每个child加载到如果child对象很大和/或父对象有大量child对象,则可能会占用大量内存,这实际上取决于您的用例。

If you decided you wanted to avoid loading all of those children , you could do a straight DB hit every time using the count query depicted in method 2. In fact, you would probably actually want to relocate that to a scope in Child as perhaps some would consider it an anti-pattern to do queries like that outside of the target model, but this just makes it easier to see for the example. 如果您决定避免加载所有这些children ,则可以使用方法2中描述的count查询每次进行一次直接数据库命中。实际上,您实际上可能希望将其重定位到Child的作用域,例如会认为在目标模型之外执行类似的查询是一种反模式,但这只是使示例更容易看到。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM