Question

My rails 4 application has many Measures which belong to a Station. I tried to eager load the measures in my controller:

 @station = Station.includes(:measures).friendly.find(params[:id])
 @measures = @station.measures

However when I added a method to the measure model which access a property of the station it causes an additional query per measure:

 SELECT "stations".* FROM "stations" WHERE "stations"."id" = ? ORDER BY "stations"."id" ASC LIMIT 1 

This means about 50 queries per page load which totally tanks performance.

How do I properly eager load the relationship to avoid this n+1 query? Am I going about it wrong?


github: /app/controllers/stations_controller.rb

class StationsController < ApplicationController
  ...

  # GET /stations/1
  # GET /stations/1.json
  def show
    @station = Station.includes(:measures).friendly.find(params[:id])
    @measures = @station.measures
  end

  ...
end

github: /app/models/measure.rb

class Measure < ActiveRecord::Base
  belongs_to :station, dependent: :destroy, inverse_of: :measures
  after_save :calibrate!
  after_initialize :calibrate_on_load

  ...

  def calibrate!
    # this causes the n+1 query
    unless self.calibrated
      unless self.station.speed_calibration.nil?
        self.speed            = (self.speed * self.station.speed_calibration).round(1)
        self.min_wind_speed   = (self.min_wind_speed * self.station.speed_calibration).round(1)
        self.max_wind_speed   = (self.max_wind_speed * self.station.speed_calibration).round(1)

        self.calibrated = true
      end
    end
  end

  def calibrate_on_load
    unless self.new_record?
      self.calibrate!
    end
  end

  def measure_cannot_be_calibrated
    if self.calibrated
      errors.add(:speed_calbration, "Calibrated measures cannot be saved!")
    end
  end

end

github: /app/models/stations.rb

class Station < ActiveRecord::Base

  # relations
  belongs_to :user, inverse_of: :stations
  has_many  :measures, inverse_of: :station, counter_cache: true

  # slugging
  extend FriendlyId
  friendly_id :name, :use => [:slugged, :history]

  ...

end

ADDITION It interesting to note that this does not cause a n+1 query. But I would rather not duplicate it across my controllers.

 class Measure < ActiveRecord::Base
      ...
      # after_initialize :calibrate_on_load
      ...
 end      


 @station = Station.includes(:measures).friendly.find(params[:id])
 @measures = @station.measures
 @measures.each do |m|
   m.calibrate!
 end
Was it helpful?

Solution 3

after_initialize occurs before relations are eager-loaded by joins or include. See https://github.com/rails/rails/issues/13156

I decided to after some good advice use a dfferent approach and came up with this:

class Station < ActiveRecord::Base

  ...

  alias_method :measures_orig, :measures
  def measures
      measures_orig.map do |m|
        m.calibrate!
      end
  end

end

OTHER TIPS

Try to include station to measure as well.

def show
  @station = Station.includes(:measures).friendly.find(params[:id])
  @measures = @station.measures.includes(:station)
end

The :includes option will usually trigger loads in separate queries as you are seeing, because in many cases it is more performant. If you want to ensure it's done in one query, try using :joins instead, but beware that the resulting records returned will be read-only.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top