简体   繁体   中英

Preventing N+1 in Rails query with associated model

I have these models:

class KlassOccurrence < ApplicationRecord
  belongs_to :klass
end

class Klass < ApplicationRecord
  has_many :klass_occurrences
  belongs_to :user

  def next_occurrence_date
    self.klass_occurrences.order(scheduled_at: :desc).first
  end
end

And I have this in a controller:

@klasses = Klass.includes(:klass_occurrences).where(user_id: current_user.id)

And in a view I have this:

   <% @klasses.each do |klass| %>
      <tr>
        <td><%= klass.next_occurrence_date %></td>
      </tr>
    <% end %>

I would expect that because of the includes(:klass_occurrences) I wouldn't be seeing an N+1, but this is what I see in the logs:

User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2  [["id", 3], ["LIMIT", 1]]
  ↳ app/controllers/klasses_controller.rb:4:in `index'
  Rendering klasses/index.html.erb within layouts/dashboard_layout
  Klass Load (0.2ms)  SELECT "klasses".* FROM "klasses" WHERE "klasses"."user_id" = $1  [["user_id", 3]]
  ↳ app/views/klasses/index.html.erb:13
  KlassOccurrence Load (4.3ms)  SELECT "klass_occurrences".* FROM "klass_occurrences" WHERE "klass_occurrences"."klass_id" IN ($1, $2)  [["klass_id", 1], ["klass_id", 5]]
  ↳ app/views/klasses/index.html.erb:13
  KlassOccurrence Load (1.8ms)  SELECT "klass_occurrences".* FROM "klass_occurrences" WHERE "klass_occurrences"."klass_id" = $1 ORDER BY "klass_occurrences"."scheduled_at" DESC LIMIT $2  [["klass_id", 1], ["LIMIT", 1]]
  ↳ app/models/klass.rb:6:in `next_occurrence_date'
  KlassOccurrence Load (0.3ms)  SELECT "klass_occurrences".* FROM "klass_occurrences" WHERE "klass_occurrences"."klass_id" = $1 ORDER BY "klass_occurrences"."scheduled_at" DESC LIMIT $2  [["klass_id", 5], ["LIMIT", 1]]
  ↳ app/models/klass.rb:6:in `next_occurrence_date'

Why is it doing three queries for klass occurrences? I would expect just one query to load all the occurrences for all the klasses, but then I see two extra queries.

If you just need the highest value of that column you can also just select an aggregate off the join table:

class Klass < ApplicationRecord
  has_many :klass_occurrences
  belongs_to :user

  def self.with_next_occurrence_date
    self.select(
         'klass.*',
         'MAX(klass_occurrences.scheduled_at) AS next_occurrence_date'
        )
        .left_joins(:klass_occurrences)
        .group(:id)
  end 
end
@klasses = Klass.with_next_occurrence_date.where(user_id: current_user.id)
<% @klasses.each do |klass| %>
  <tr>
    <td><%= klass.next_occurrence_date %></td>
  </tr>
<% end %>

If you really need a model object you can do it by sorting the records in Ruby:

class Klass < ApplicationRecord
  has_many :klass_occurrences
  belongs_to :user
  def next_occurrence_date
    self.klass_occurrences.sort(&:scheduled_at).last
  end
end

And there are also a bunch of novel tricks to get ActiveRecord to load just one record off the association that are somewhat out of scope for this question.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM