【问题标题】:Rails 4, ActiveRecord SQL subqueriesRails 4,ActiveRecord SQL 子查询
【发布时间】:2015-09-04 10:26:00
【问题描述】:

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

这将返回所有父母的最大年龄数组;我想将其限制为仅 1 个父级,或者在检索所有父级时将其作为属性包含在父级上。

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

【问题讨论】:

    标签: sql ruby-on-rails-4 subquery select-n-plus-1


    【解决方案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 方法。

    然而,两个缺点是,如果在子实例化后底层数据发生了变化,它仍然会使用过时的结果(这可以通过在调用 :children 时传递 :true 来避免。另外,它是首先将每个child 加载到内存中,然后对其进行计数。如果child 对象很大和/或父对象有大量子对象,则可能会占用大量内存。这实际上取决于您的用例。

    如果您决定要避免加载所有这些 children,则可以使用方法 2 中描述的 count 查询每次都直接进行 DB 命中。事实上,您可能实际上希望将其重新定位到Child 中的一个范围,因为也许有些人会认为在目标模型之外进行类似的查询是一种反模式,但这只是为了便于查看示例。

    【讨论】:

    • W:我想出了一个使用关联的可行解决方案(见编辑);考虑到我每次检索父母时都希望执行此计算,您的上述第二种方法会是更有效的解决方案吗?
    • @DanL 我强烈建议不要那样做。您以这种方式不必要地对数据库进行非规范化,并冒着各种奇怪的同步问题的风险。如果您想要返回实际的子对象,因为您不仅想要年龄,只需将其设为方法而不是这样的关联。这样你就可以打电话给parent.oldest_child,然后你可以打电话给parent.old_child.age
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-02-15
    • 1970-01-01
    • 2018-08-19
    • 1970-01-01
    • 2013-10-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多