Victor
Victor

Reputation: 13388

Optimize eager loading in Rails

Rails 3.2. I have the following:

# city.rb
class City < ActiveRecord::Base
  has_many :zones, :dependent => :destroy
end

# zone.rb
class Zone < ActiveRecord::Base
  belongs_to :city
  has_many :zone_shops, :dependent => :destroy
  has_many :shops, :through => :zone_shops
end

# zone_shop.rb
class ZoneShop < ActiveRecord::Base
  belongs_to :zone
  belongs_to :shop
end

# shop.rb
class Shop < ActiveRecord::Base
end

# cities_controller.rb
def show
  @trip = City.find(params[:id], :include => [:user, :zones => [:shops]])
  @zones = @trip.zones.order("position")

  # List out all shops for the trip
  shops_list_array = []
  @zones.each do |zone, i|
    zone.shops.each do |shop|
      shops_list_array << shop.name
    end
  end
  @shops_list = shops_list_array.join(', ')
end

# development.log
  City Load (0.3ms)  SELECT `cities`.* FROM `cities` WHERE `cities`.`id` = 1 LIMIT 1
  Zone Load (0.3ms)  SELECT `zones`.* FROM `zones` WHERE `zones`.`trip_id` IN (1) ORDER BY position asc
  ZoneShop Load (0.3ms)  SELECT `zone_shops`.* FROM `zone_shops` WHERE `zone_shops`.`zone_id` IN (26, 23, 22) ORDER BY position asc
  Shop Load (0.5ms)  SELECT `shops`.* FROM `shops` WHERE `shops`.`id` IN (8, 7, 1, 9)
  Zone Load (0.5ms)  SELECT `zones`.* FROM `zones` WHERE `zones`.`trip_id` = 1 ORDER BY position asc, position
  Shop Load (0.5ms)  SELECT `shops`.* FROM `shops` INNER JOIN `zone_shops` ON `shops`.`id` = `zone_shops`.`spot_id` WHERE `zone_shops`.`zone_id` = 26
  Shop Load (0.6ms)  SELECT `shops`.* FROM `shops` INNER JOIN `zone_shops` ON `shops`.`id` = `zone_shops`.`spot_id` WHERE `zone_shops`.`zone_id` = 23
  Shop Load (0.4ms)  SELECT `shops`.* FROM `shops` INNER JOIN `zone_shops` ON `shops`.`id` = `zone_shops`.`spot_id` WHERE `zone_shops`.`zone_id` = 22

Notice in my log, the last 3 lines with shops with ID 26, 23, 22 are redundant. How should I rewrite my cities_controller.rb to reduce the query to the system?

Many thanks.

Upvotes: 2

Views: 1537

Answers (2)

emrass
emrass

Reputation: 6444

@zones = @trip.zones.includes(:shops).order("position")

This eager-loads the shops association and should elimitate the n+1 query problem caused by zone.shops.each

For more information, have a look at the Ruby on Rails Guide section 12 on Eager Loading associations, which was also linked by @Benjamin M

Upvotes: 2

Benjamin M
Benjamin M

Reputation: 24567

I'd suggest that this

zone.shops.each do |shop|
  shops_list_array << shop.name
end

produces the 3 last lines of your log. This means: You currently have only one zone inside your database. If you put more zones in there, you will get a lot more Zone Load entries in log.

The problem obviously is Rails' each method, which triggers the lazy loading:

@zones.each do |zone, i|
  ...

The solution depends on your needs, but I'd suggest that you read everything about Rails' eager loading feature. (There's exactly your problem: The each thing). Look it up here: http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations

It's pretty easy, short and straightforward :)

Upvotes: 0

Related Questions