Vecchia Spugna
Vecchia Spugna

Reputation: 682

Rails4: eager loading not working

I can't understand when eager loading is sufficient and when it's not. I'll make a practical example: take the models on the image below, I must get the teams that have played a certain match (so I need some joins):

In my MatchesController I do the following:

@matches = Match.scheduled_or_playing
.joins(match_locations: {scores: {team_match: :team}})
.includes(match_locations: {scores: {team_match: :team}})

and in my index view I do the following:

<% @matches.each do |m| %>
    <%= m.teams.map(&:name).join " | "  %> 
<% end

This triggers a lot of queries, as shown in this log:

  SQL (5.1ms)  SELECT "matches"."id" AS t0_r0, "matches"."server_id" AS t0_r1, "matches"."pool_id" AS t0_r2, "matches"."state" AS t0_r3, "matches"."can_draw" AS t0_r4, "matches"."scheduled_at" AS t0_r5, "matches"."created_at" AS t0_r6, "matches"."updated_at" AS t0_r7, "match_locations"."id" AS t1_r0, "match_locations"."match_id" AS t1_r1, "match_locations"."location_id" AS t1_r2, "match_locations"."created_at" AS t1_r3, "match_locations"."updated_at" AS t1_r4, "scores"."id" AS t2_r0, "scores"."match_location_id" AS t2_r1, "scores"."team_match_id" AS t2_r2, "scores"."points" AS t2_r3, "scores"."created_at" AS t2_r4, "scores"."updated_at" AS t2_r5, "team_matches"."id" AS t3_r0, "team_matches"."team_id" AS t3_r1, "team_matches"."created_at" AS t3_r2, "team_matches"."updated_at" AS t3_r3, "teams"."id" AS t4_r0, "teams"."encrypted_password" AS t4_r1, "teams"."remember_created_at" AS t4_r2, "teams"."sign_in_count" AS t4_r3, "teams"."current_sign_in_at" AS t4_r4, "teams"."last_sign_in_at" AS t4_r5, "teams"."current_sign_in_ip" AS t4_r6, "teams"."last_sign_in_ip" AS t4_r7, "teams"."ts_address" AS t4_r8, "teams"."ts_password" AS t4_r9, "teams"."picture_url" AS t4_r10, "teams"."name" AS t4_r11, "teams"."username" AS t4_r12, "teams"."created_at" AS t4_r13, "teams"."updated_at" AS t4_r14 FROM "matches" INNER JOIN "match_locations" ON "match_locations"."match_id" = "matches"."id" INNER JOIN "scores" ON "scores"."match_location_id" = "match_locations"."id" INNER JOIN "team_matches" ON "team_matches"."id" = "scores"."team_match_id" INNER JOIN "teams" ON "teams"."id" = "team_matches"."team_id" WHERE "matches"."state" IN (1, 2) ORDER BY matches.scheduled_at ASC
  Team Load (1.1ms)  SELECT "teams".* FROM "teams" INNER JOIN "team_matches" ON "teams"."id" = "team_matches"."team_id" INNER JOIN "scores" ON "team_matches"."id" = "scores"."team_match_id" INNER JOIN "match_locations" ON "scores"."match_location_id" = "match_locations"."id" WHERE "match_locations"."match_id" = $1  [["match_id", 56]]
  Team Load (1.1ms)  SELECT "teams".* FROM "teams" INNER JOIN "team_matches" ON "teams"."id" = "team_matches"."team_id" INNER JOIN "scores" ON "team_matches"."id" = "scores"."team_match_id" INNER JOIN "match_locations" ON "scores"."match_location_id" = "match_locations"."id" WHERE "match_locations"."match_id" = $1  [["match_id", 68]]

  ....(n times)

  Team Load (1.5ms)  SELECT "teams".* FROM "teams" INNER JOIN "team_matches" ON "teams"."id" = "team_matches"."team_id" INNER JOIN "scores" ON "team_matches"."id" = "scores"."team_match_id" INNER JOIN "match_locations" ON "scores"."match_location_id" = "match_locations"."id" WHERE "match_locations"."match_id" = $1  [["match_id", 47]]

But, if i modify the view in this way...

<% @matches.each do |m| %>
    <%= m.teams  %> 
<% end %>

the view doesn't produce the same result, but it SHOULD load the same classes and attrs. However, magically, now eager loading works, here's the log

 SQL (4.9ms)  SELECT "matches"."id" AS t0_r0, "matches"."server_id" AS t0_r1, "matches"."pool_id" AS t0_r2, "matches"."state" AS t0_r3, "matches"."can_draw" AS t0_r4, "matches"."scheduled_at" AS t0_r5, "matches"."created_at" AS t0_r6, "matches"."updated_at" AS t0_r7, "match_locations"."id" AS t1_r0, "match_locations"."match_id" AS t1_r1, "match_locations"."location_id" AS t1_r2, "match_locations"."created_at" AS t1_r3, "match_locations"."updated_at" AS t1_r4, "scores"."id" AS t2_r0, "scores"."match_location_id" AS t2_r1, "scores"."team_match_id" AS t2_r2, "scores"."points" AS t2_r3, "scores"."created_at" AS t2_r4, "scores"."updated_at" AS t2_r5, "team_matches"."id" AS t3_r0, "team_matches"."team_id" AS t3_r1, "team_matches"."created_at" AS t3_r2, "team_matches"."updated_at" AS t3_r3, "teams"."id" AS t4_r0, "teams"."encrypted_password" AS t4_r1, "teams"."remember_created_at" AS t4_r2, "teams"."sign_in_count" AS t4_r3, "teams"."current_sign_in_at" AS t4_r4, "teams"."last_sign_in_at" AS t4_r5, "teams"."current_sign_in_ip" AS t4_r6, "teams"."last_sign_in_ip" AS t4_r7, "teams"."ts_address" AS t4_r8, "teams"."ts_password" AS t4_r9, "teams"."picture_url" AS t4_r10, "teams"."name" AS t4_r11, "teams"."username" AS t4_r12, "teams"."created_at" AS t4_r13, "teams"."updated_at" AS t4_r14 FROM "matches" INNER JOIN "match_locations" ON "match_locations"."match_id" = "matches"."id" INNER JOIN "scores" ON "scores"."match_location_id" = "match_locations"."id" INNER JOIN "team_matches" ON "team_matches"."id" = "scores"."team_match_id" INNER JOIN "teams" ON "teams"."id" = "team_matches"."team_id" WHERE "matches"."state" IN (1, 2) ORDER BY matches.scheduled_at ASC

I really cannot understand why

EDIT:

as requested, I add the models here:

Team

class Team < ActiveRecord::Base  
  has_many :team_matches
  has_many :matches, :through => :team_matches
end

TeamMatch

class TeamMatch < ActiveRecord::Base
  belongs_to :team
  has_many :scores, :dependent => :destroy, :inverse_of => :team_match
  has_many :match_locations, through: :scores
end

Score

class Score < ActiveRecord::Base
  belongs_to :team_match
  belongs_to :match_location
end

MatchLocation

class MatchLocation < ActiveRecord::Base
  belongs_to :location
  belongs_to :match      
  has_many :scores
  accepts_nested_attributes_for :scores
  has_many :team_matches, through: :scores
end

Match

class Match < ActiveRecord::Base  
  has_many :match_locations
  accepts_nested_attributes_for :match_locations, allow_destroy: true 
  has_many :scores, through: :match_locations
  has_many :team_matches, through: :scores
  has_many :teams, through: :team_matches
end

models relations

Upvotes: 1

Views: 1140

Answers (2)

Wizard of Ogz
Wizard of Ogz

Reputation: 12643

I think ActiveRecord is not recognizing the way you want eager loading teams through nested through associations. Try just including teams at the "match level" rather than nested.

@matches = Match.scheduled_or_playing
  .joins(:teams)
  .includes(:teams)

If you need all that other nested eager loading then simply include it back in:

@matches = Match.scheduled_or_playing
  .joins([:teams, {match_locations: {scores: {team_match: :team}}}])
  .includes([:teams, {match_locations: {scores: {team_match: :team}}}])

Check your logs to make sure the queries and eager loading are working as expected (including performance).

Upvotes: 1

Srikanth Venugopalan
Srikanth Venugopalan

Reputation: 9049

There are multiple things to note here.

Firstly, you probably do not need to use joins and includes together. They are similar but not the same.

includes does an eager fetch, whereas joins does not. Note that in your case you have joins before includes. So the association is fetched using join.

Next, looking at your different views,

When you do a m.teams, you are only referring to the association, not the actual teams collection. However, in your first view when you do a m.teams.map... you are now referring the attributes of the association, which causes ActiveRecord to fetch the Team objects, as required (i.e. lazy). Hence the N+1 query problem.

Ryan Bates has an excellent Rails Cast to compare this.

Upvotes: 1

Related Questions