让我们先从最明显的方法开始:
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=1
Task.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的缓存记录。1
2
3
Task.find(1)
tasks
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
默认情况下从未处于活动状态(有充分的理由),并最终从 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] }