Rails 5.1(PostgreSQL):高效查询项目及当前用户关联任务的方案
嘿,这个N+1查询的坑我踩过好多次了,咱们来快速搞定它!首先得明确你的模型关系:Project has_many :tasks,Task belongs_to :project且belongs_to :user对吧?原来的遍历方式之所以会触发重复查询,是因为每循环一个项目就去执行一次project.tasks.where(user_id: current_user.id),相当于每个项目都单独发一次SQL请求,项目多了性能直接拉胯。
下面给你两种高效的解决方案,按需选择就行:
方案一:从Project出发,预加载过滤后的用户任务
这种方式适合你需要先展示项目列表,再在每个项目下列出用户任务的场景,核心是用**预加载(Eager Loading)**结合条件过滤,一次性把所有需要的数据查出来:
# 获取所有包含当前用户任务的项目,同时预加载该用户的任务 @projects = Project.joins(:tasks) .where(tasks: { user_id: current_user.id }) .distinct .includes(:tasks) .where(tasks: { user_id: current_user.id })
代码解释:
joins(:tasks):通过内连接只筛选出包含用户任务的项目,排除没有用户任务的项目;.distinct:因为一个项目可能有多个用户任务,内连接会生成重复的项目记录,用这个去重;includes(:tasks).where(tasks: { user_id: current_user.id }):预加载时只加载当前用户的任务,而不是项目的所有任务,避免不必要的数据加载。
这样查询完成后,你在视图里直接遍历@projects,调用project.tasks就不会再触发新的SQL查询了,所有数据已经在内存里了。
如果你想强制用一次LEFT JOIN查询(而不是可能的两次查询),可以把includes换成eager_load,代码更简洁:
@projects = Project.eager_load(:tasks) .where(tasks: { user_id: current_user.id }) .distinct
方案二:从Task入手,按Project分组展示
这种方式逻辑更直观:先获取当前用户的所有任务,预加载关联的项目,然后在视图里按项目分组展示。代码量更少,适合用户任务数量不多的场景:
# 获取当前用户的所有任务,同时预加载对应的项目 @user_tasks = current_user.tasks.includes(:project)
然后在视图里(比如ERB)分组展示:
<% @user_tasks.group_by(&:project).each do |project, tasks| %> <h3><%= project.name %></h3> <ul> <% tasks.each do |task| %> <li><%= task.title %> - <%= task.status %></li> <% end %> </ul> <% end %>
这种方式只需要一次SQL查询就能拿到所有需要的任务和项目数据,完全避免N+1问题,而且代码逻辑更易懂。
为什么原来的方式会有问题?
原来的流程大概是:
- 先查所有包含用户任务的项目:
Project.where(id: current_user.tasks.pluck(:project_id))(这是1次查询) - 然后循环每个项目,调用
project.tasks.where(user_id: current_user.id)(每个项目触发1次查询,N个项目就是N次)
总共触发1+N次SQL查询,也就是所谓的N+1问题,数据量一大就会严重拖慢页面加载速度。而上面两种方案都只需要1次查询,性能提升非常明显。
内容的提问来源于stack exchange,提问作者Dan L

