Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby on Rails - Bullet/N+1

On Rails 4. I recently installed the bullet gem for my development environment to clear up my app's N+1 queries. Relevant models:

Submissions: Belongs to Categories and Users. Has many SubmissionDetails.

Users: Has many Submissions

Categories: Has many Submissions. Belongs to Awards.

Awards: Has many Categories (and Submissions through Categories)

SubmissionDetails: Belongs to Submissions

In my submission's index page, I have an each do statement to display each submission made by the current user.

<% current_user.submissions.order('created_at DESC').in_groups_of(3, false) do |group| %>
  <div class="row">
    <% group.each do |submission| %>

After that, I list information about the submission, including its associated category, award, and submission details information. Bullet is saying I'm having N+1 issues with this statement:

N+1 Query detected Submission => [:category] Add to your finder: :include => [:category]
N+1 Query detected Submission => [:user] Add to your finder: :include => [:user]
N+1 Query detected Submission => [:submission_details] Add to your finder: :include => [:submission_details]

Every time I try to add .includes with all three of those models, it only picks the first one I list (this is not surprising). I figure I need to go a different route when multiple models are involved--perhaps a join statement?

(When I make :category the first item, it adds this notice):

N+1 Query detected Category => [:award] Add to your finder: :include => [:award]

(So I also need to include as part of the statement a way to make Award fit in there as well, which, again, has many Submissions through Categories).

So assuming I can't do one .includes for three different models, is there another way to go about this? Thanks.

like image 548
Rachel9494 Avatar asked May 20 '14 21:05

Rachel9494


2 Answers

Just to be more clear, let me make the details more visible:

class User < ActiveRecord::Base
  has_many :submissions
end

class Submission < ActiveRecord::Base
  belongs_to :category
  belongs_to :user
  has_many   :submission_details
end

class SubmissionDetail < ActiveRecord::Base
  belongs_to :submission
end

class Category < ActiveRecord::Base
  belongs_to :award
  has_many   :submissions
end

class Award < ActiveRecord::Base
  has_many :categories
  has_many :submissions, through: :categories
end

If I understand correctly, for your current_user, you are listing his submissions. For each submission you want to list submission_details and the category it belongs. For every category you list the award too.

<% current_user.submissions.order('created_at DESC').in_groups_of(3, false) do |group| %>
  <div class="row">
    <% group.each do |submission| %>
      ...
      <div><%= submission.category %></div>
      <div><%= submission.category.award %></div>
      <%= submission.submissions_details.each do |submission_detail| %>
        ...
      <% end %>
    <% end %>
  </div>
<% end %> 

You can remove N+1 problem by using includes in the following manner:

current_user.submissions.includes(:submission_details, :category => :award)

For more details about includes please refer to:

  1. Rails guides eager-loading-associations
  2. Rails guides eager-loading-multiple-associations
  3. Rails api - includes

associations

like image 158
cristian Avatar answered Sep 20 '22 01:09

cristian


To include those associations, I would create a scope for submissions. While you're at it, add a latest scope.

class Submission
  scope :eager,  -> { includes(:submission_details, :category => [:award]) }
  scope :latest, -> { order("created_at DESC") }
end

Then simply

current_user.submissions.latest.eager [...]

You shouldn't have to include :user, but I've noticed Rails isn't too clever about such references.

like image 28
Damien Roche Avatar answered Sep 18 '22 01:09

Damien Roche