开发者

Eager Loading with "has many through" -- do I need Arel?

开发者 https://www.devze.com 2023-04-05 15:58 出处:网络
I have three tables: users, members, projects.The middle is a join table expressing a has-many-through between the other two tables; and it has some attributes of interest, including join_code and act

I have three tables: users, members, projects. The middle is a join table expressing a has-many-through between the other two tables; and it has some attributes of interest, including join_code and activated.

More expansively:

class User < ActiveRecord::Base
  has_many :members
  has_many :projects, :through => :members
end

class Member < ActiveRecord::Base
  belongs_to :user
  belongs_to :project
  # has a column called join_code
  # has a column called activated
  # Note that this class can be thought of as "membership"
end

class Project < ActiveRecord::Base
  has_many :members
  has_many :use开发者_开发知识库rs, :through => :members
end

Goal: Given a particular user, I want a query that will get all the projects, and eager load only the member records that link those projects to the user.

So far I have this method in user.rb that does a query:

def live_projects
  self.projects.order("projects.name").includes(:members).where(:members => {:join_code => nil, :activated => true})
end

But it's not enough. I'd like to then be able to do this in the view code:

<% current_user.live_projects.each do |project| %>
  <li project_id="<%= project.id %>">
    <% member = project.member %>
      (Do something with that member record here)
      <%= project.name %>
    <% end %>
  </li>
<% end %>

Here, normally, I'd have project.members, but in my context I'm only interested in that one member record that links back to the user.

Here is what I think the raw SQL should look like

select projects.*, members.* 
from projects inner join members on projects.id = members.project_id
where members.user_id = X and members.join_code is null and members.activated = 't';

How to do that in Arel (or ActiveRecord)?


I may have something of an answer here, namely that the ActiveRecord code I wrote seems pretty reasonable. Again, here's that query:

def live_projects
  self.projects.order("projects.name").includes(:members).where(:members => {:join_code => nil, :activated => true})
end

On a run through the UI with sample data it generates this output from Rails server:

Project Load (0.6ms)  SELECT "projects".* FROM "projects" INNER JOIN "members" ON "projects".id = "members".project_id WHERE "members"."join_code" IS NULL AND "members"."activated" = 't' AND (("members".user_id = 3)) ORDER BY projects.name
Member Load (2.0ms)  SELECT "members".* FROM "members" WHERE ("members".project_id IN (50,3,6,37,5,1))

Then later in the view code I can do this:

<% current_user.live_projects.each do |project| %>
  <li project_id="<%= project.id %>" class="<%= 'active' if project == @project %>">
    <% member = project.members.detect { |member| member.user_id == current_user.id } %>
    (Do something with that member record here)
  </li>
<% end %>

That expression to get the member record is pretty ugly in the view, but select is an Array method, not a query, and no extra DB hits other than the two shown above appear in the output from Rails server. Thus I guess my n+1 problem is solved.


Add an association called live_members on the Project class.

class Project < ActiveRecord::Base
  has_many :live_members, :class_name => "Member", 
                :conditions => {:join_code => nil, :activated => true}
  has_many :members
  has_many :users, :through => :members
end

Add an association called live_projects on the User class.

class User < ActiveRecord::Base
  has_many :members
  has_many :projects, :through => :members
  has_many :live_projects, :through => :members, :source => :project, 
             :include => :live_member, :order => "projects.name"
end

Now you can:

user.live_projects


It seems that you expect there to be at most one active member linking each user to a project. If this is the case the following should work:

In member.rb:

scope :live, where(:join_code => nil, :activated => true)

In user.rb:

def live_projects_with_members
  members.live.includes(:project).group_by(&:project)
end

In your view:

<% current_user.live_projects_with_members.each do |project, members| %>
  <% member = members.first %>
  <li project_id="<%= project.id %>" class="<%= 'active' if project == @project %>">
      (Do something with that member record here)
  </li>
<% end %>

If you then want to add an extra join for your usage stats you can do this:

def live_projects_with_members
  members.live.includes(:project, :stats).group_by(&:project)
end
0

精彩评论

暂无评论...
验证码 换一张
取 消