Reputation: 682
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
Upvotes: 1
Views: 1140
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
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