(是:在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))
然后我将它减少到任务集并丢失我的集合的顺序。那更清楚吗?
答案 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 1
,2
和3
的条目,并且后续的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
- 它只会在内存中引用相同的对象。