I’m trying to avoid the N+1 queries problem with eager loading, but it’s not working. The associated models are still being loaded individually.
Here are the relevant ActiveRecords and their relationships:
class Player < ActiveRecord::Base
has_one :tableau
end
Class Tableau < ActiveRecord::Base
belongs_to :player
has_many :tableau_cards
has_many :deck_cards, :through => :tableau_cards
end
Class TableauCard < ActiveRecord::Base
belongs_to :tableau
belongs_to :deck_card, :include => :card
end
class DeckCard < ActiveRecord::Base
belongs_to :card
has_many :tableaus, :through => :tableau_cards
end
class Card < ActiveRecord::Base
has_many :deck_cards
end
class Turn < ActiveRecord::Base
belongs_to :game
end
and the query I’m using is inside this method of Player:
def tableau_contains(card_id)
self.tableau.tableau_cards = TableauCard.find :all, :include => [ {:deck_card => (:card)}], :conditions => ['tableau_cards.tableau_id = ?', self.tableau.id]
contains = false
for tableau_card in self.tableau.tableau_cards
# my logic here, looking at attributes of the Card model, with
# tableau_card.deck_card.card;
# individual loads of related Card models related to tableau_card are done here
end
return contains
end
Does it have to do with scope? This tableau_contains method is down a few method calls in a larger loop, where I originally tried doing the eager loading because there are several places where these same objects are looped through and examined. Then I eventually tried the code as it is above, with the load just before the loop, and I’m still seeing the individual SELECT queries for Card inside the tableau_cards loop in the log. I can see the eager-loading query with the IN clause just before the tableau_cards loop as well.
EDIT: additional info below with the larger, outer loop
EDIT2 : corrected loop below with tips from answers
EDIT3 : added more details in loop with goals
Here’s the larger loop. It is inside an observer on after_save
def after_save(pa)
turn = Turn.find(pa.turn_id, :include => :player_actions)
game = Game.find(turn.game_id, :include => :goals)
game.players.all(:include => [ :player_goals, {:tableau => [:tableau_cards => [:deck_card => [:card]]]} ])
if turn.phase_complete(pa, players) # calls player.tableau_contains(card)
for goal in game.goals
if goal.checks_on_this_phase(pa)
if goal.is_available(players, pa, turn)
for player in game.players
goal.check_if_player_takes(player, turn, pa)
... # loop through player.tableau_cards
end
end
end
end
end
end
Here’s the relevant code in the turn class:
def phase_complete(phase, players)
all_players_complete = true
for player in players
if(!player_completed_phase(player, phase))
all_players_complete = false
end
end
return all_players_complete
end
the for player in game.players is doing another query to load the players. It is cached, I mean it has the CACHE label in the log, but I would’ve thought there would be no query at all because the game.players should already be loaded in memory.
Another snippet from the Goal model:
class Goal < ActiveRecord::Base
has_many :game_goals
has_many :games, :through => :game_goals
has_many :player_goals
has_many :players, :through => :player_goals
def check_if_player_takes(player, turn, phase)
...
for tab_card in player.tableau_cards
...
end
end
Try this:
Change the logic of
tableau_containsas follows:Change the logic of
after_saveas follows:Second line in the
after_savemethod eager loads the data needed to perform thetableau_containscheck. The calls such astableau.tableau_cardsandtc.deck_card.cardshould/will not hit the DB.Issues in your code:
1) Assigning array to an
has_manyassociationStatement above is not a simple assignment statement. It changes the
palyerstable rows with thegame_idof the given game.I am assuming that is not what you want. If you check the DB table you will notice that the
updated_timeof the players tablerows have changed after assignment.
You have to assign the value to a separate variable as shown in the code sample in
after_savemethod.2) Hand coded association SQL
Many places in the code you are hand coding the SQL for association data. Rails provides associations for this.
E.g:
Can be rewritten as:
The
tableau_cardscards association onTableaumodel constructs the same SQL you have hand coded.You can further improve the statement above by adding a
has_many :throughassociation toPlayerclass.Edit 1
I created an application to test this code. It works as expected. Rails runs several SQL to eager load the data, i.e.:
I don’t see any SQL executed after eager loading the data.
Edit 2
Make the following change to your code.
The caching works as follows:
The second statement above results in a custom association query. Hence AR doesn’t cache the results. Where as
player_goalsare cached for every player object in the 3rd statement as they are fetched using standard association SQL.