2

(原为:ActiveRecord 中的反向急切加载

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

代码

class Task < ActiveRecord::Base
 belongs_to :project

class Project < ActiveRecord::Base
 has_many :tasks

问题

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

我能做些什么来避免这种混乱的局面?

编辑

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

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

但后来我将它减少到任务集并丢失了我的集合的顺序。这更清楚吗?

4

3 回答 3

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 它使用了查询缓存。在( ) 之后运行Task.find(1)( ) 将不会利用查询缓存,因为第一个查询与第二个查询不同。(不过,运行第二次、第三次等时间利用查询缓存,因为 Rails 将多次看到完全相同的查询并返回缓存的结果集。)SELECT * FROM tasks WHERE id=1Task.find([1,2,3])SELECT * FROM tasks WHERE id IN (1,2,3)Task.find(1)SELECT

进入IdentityMap缓存。Identity Map Caching 的不同之处在于它在每个表和主键的基础上缓存记录,而不是查询。因此,runningTask.find([1,2,3])将填写 Identity Map Cache for table 中的三个记录(分别具有 ID 和IDtasks的条目),随后将立即返回 table和 ID的缓存记录。123Task.find(1)tasks1

# 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 默认情况下从未处于活动状态(有充分的理由),并最终从 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] }
于 2012-11-27T03:53:33.230 回答
0

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

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

project_ids = @tasks.map{|task| task.project_id}.uniq
@projects = Project.find(project_ids)
于 2012-11-26T06:06:38.013 回答
0

如果您通过以下行在 Rails 中启用 IdentityMap config/application.rb

config.active_record.identity_map = true

然后 ActiveRecord 实际上不会返回数据库来加载Project它之前已经加载的数据——它只会引用内存中的同一个对象。

于 2012-11-27T17:44:34.597 回答