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
中的范围,因为也许有些人会认为在目标模型之外进行类似查询的反模式,但这只是让示例更容易看到。
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
中的范围,因为也许有些人会认为在目标模型之外进行类似查询的反模式,但这只是让示例更容易看到。