ActiveRecord:从主键数组加载相应的记录数组(保留顺序,重复,最大化性能)

时间:2012-11-25 23:53:15

标签: ruby-on-rails ruby-on-rails-3 activerecord eager-loading

(是:在ActiveRecord中反向加载

我有这个奇怪的问题,我知道我需要使用急切的加载,但由于这是一个非常奇怪的用例,它不能很好地工作。

守则

class Task < ActiveRecord::Base
 belongs_to :project

class Project < ActiveRecord::Base
 has_many :tasks

问题

我知道在传统设置中你有一个Project并且想要渲染任务,你使用eager-load来加载任务一次,而不是按顺序迭代它们。但是,在我的情况下,我有一个任务列表,每个任务我需要获取适当的项目。顺序渲染时,Rails SQL缓存会有所帮助,但是我有很多任务,所以我最终一遍又一遍地加载同一个Project。

我可以做些什么来避免这种混乱的情况?

修改

我试图澄清情况。我有多个任务ID数组。即

type_a_tasks = [1,2,3,1,2,3]
type_b_tasks = [1,2,2,3,3]

请注意,可以执行相同的任务。现在,我希望像函数式编程一样映射列表,这样我就得到了实际的任务,而不是id,它们的关联

type_a_tasks = [Task #1, Task #2, etc.]
type_b_tasks = [Task #1, Task #2, etc.]

我知道我可以通过

来完成任务
Task.includes(:project).find(task_a_tasks.concat(task_b_tasks))

然后我将它减少到任务集并丢失我的集合的顺序。那更清楚吗?

3 个答案:

答案 0 :(得分:2)

首先让我们从最明显的方法开始:

type_a_task_ids = [1,2,3,1,2,3]
type_b_task_ids = [1,2,2,3,3] 
type_a_tasks = type_a_task_ids.map { |task_id| Task.includes(:project).find(task_id) }
type_b_tasks = type_b_task_ids.map { |task_id| Task.includes(:project).find(task_id) }

以上内容简单易读但可能:它将为每个不同的task_id 以及一个数据库循环执行一次数据库往返 - 给定任务中每个不同project_id的行程。所有延迟都会增加,因此您希望批量加载任务(和相应的项目)。

如果您可以预先将Rails 批量加载(预取)和缓存这些相同的记录放在两个往返行程中(一个用于所有不同的任务,一个用于所有不同的任务)相关项目),然后只需要与上面完全相同的代码 - 除了find总是会点击缓存而不是数据库。

不幸的是,Rails中的事情(默认情况下)并不完全正常,ActiveRecord uses a query cache。在Task.find(1)SELECT * FROM tasks WHERE id=1)之后运行Task.find([1,2,3])SELECT * FROM tasks WHERE id IN (1,2,3))将不会利用查询缓存,因为第一个查询与第二个查询不同。 (运行Task.find(1)第二,第三等时间利用查询缓存,因为Rails会看到完全相同的SELECT查询多次飞行并返回缓存结果集。)

输入IdentityMap缓存。身份映射缓存在某种意义上是不同的,它在每个表和主键的基础上缓存记录而不是查询。因此,运行Task.find([1,2,3])将在表tasks的身份映射缓存中填写三条记录(分别具有ID 123的条目,并且后续的Task.find(1)会立即返回表tasks和ID 1的缓存记录。

# with IdentityMap turned on (see IdentityMap documentation)
# prefetch all distinct tasks and their associated projects
# throw away the result, we only want to prep the cache
Task.includes(:project).find(type_a_task_ids & type_b_task_ids)
# proceed with regular logic
type_a_task_ids = [1,2,3,1,2,3]
type_b_task_ids = [1,2,2,3,3] 
type_a_tasks = type_a_task_ids.map { |task_id| Task.includes(:project).find(task_id) }
type_b_tasks = type_b_task_ids.map { |task_id| Task.includes(:project).find(task_id) }

但是,IdentityMap has never been active by default (for good reason)was ultimately removed from Rails

如果没有IdentityMap,您如何获得相同的结果?简单:

# prefetch all distinct tasks and their associated projects
# store the result in our own identity cache
my_tasks_identity_map = \
  Hash[Task.includes(:project).find(type_a_task_ids & type_b_task_ids).map { |task|
    [ task.id, task ]
  }]
# proceed with cache-centric logic
type_a_task_ids = [1,2,3,1,2,3]
type_b_task_ids = [1,2,2,3,3] 
type_a_tasks = type_a_task_ids.map { |task_id| my_tasks_identity_map[task_id] }
type_b_tasks = type_b_task_ids.map { |task_id| my_tasks_identity_map[task_id] }

答案 1 :(得分:0)

我想我看到了你的问题,即如果你有一堆所有属于同一个项目的任务,你将多次加载该项目。

假设你已经有一个Task对象的数组,那怎么样?

project_ids = @tasks.map{|task| task.project_id}.uniq
@projects = Project.find(project_ids)

答案 2 :(得分:0)

如果您通过config/application.rb中的这一行启用Rails中的IdentityMap:

config.active_record.identity_map = true

然后ActiveRecord实际上不会回到DB来加载它之前已加载的Project - 它只会在内存中引用相同的对象。