warum wird geladen diese Schienen Verein einzeln nach einem eifrigen Last?
-
22-09-2019 - |
Frage
Ich versuche, das N + 1-Abfragen Problem mit eifrigem Laden zu vermeiden, aber es funktioniert nicht. Die zugehörigen Modelle werden noch einzeln geladen werden.
Hier sind die relevanten ActiveRecords und ihre Beziehungen:
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
und die Abfrage Ich verwende ist in diesem Verfahren des Spielers:
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
Hat es mit Rahmen zu tun? Diese tableau_contains Methode ist unten ein paar Methodenaufrufe in einer größeren Schleife, wo ich tun, die eifrig Laden ursprünglich versucht, weil es mehrere Orte, wo eben diese Objekte werden durchgeschleift und untersucht. Dann habe ich versuchte schließlich den Code, wie er oben ist, mit der Last kurz vor der Schleife, und ich bin immer noch die einzelnen SELECT-Abfragen für Card innerhalb der tableau_cards Schleife im Protokoll zu sehen. Ich kann die eifrig Lade Abfrage mit der IN-Klausel unmittelbar vor der tableau_cards Schleife als auch sehen.
EDIT: Zusatzinfo unten mit der größeren, äußeren Schleife
EDIT2: korrigiert Schleife unten mit Tipps von Antworten
EDIT3: hinzugefügt mehr Details in Schleife mit Zielen
Hier ist die größere Schleife. Es ist innerhalb eines Beobachters auf 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
Hier ist der relevante Code in der Wende-Klasse:
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
die for player in game.players
tut eine weitere Abfrage, die Spieler zu laden. Es zwischengespeichert wird, ich meine es das CACHE-Label im Protokoll hat, aber ich würde es gedacht haben würde überhaupt keine Abfrage, weil die game.players bereits in den Speicher geladen werden soll.
Ein weiterer Ausschnitt aus dem Tor Modell:
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
Lösung
Versuchen Sie diese:
class Game
has_many :players
end
Ändern Sie die Logik der tableau_contains
wie folgt:
class Player < ActiveRecord::Base
has_one :tableau
belongs_to :game
def tableau_contains(card_id)
tableau.tableau_cards.any?{|tc| tc.deck_card.card.id == card_id}
end
end
Ändern Sie die Logik der after_save
wie folgt:
def after_save(turn)
game = Game.find(turn.game_id, :include => :goals))
Rails.logger.info("Begin eager loading..")
players = game.players.all(:include => [:player_goals,
{:tableau => [:tableau_cards=> [:deck_card => [:card]]]} ])
Rails.logger.info("End eager loading..")
Rails.logger.info("Begin tableau_contains check..")
if players.any?{|player| player.tableau_contains(turn.card_id)}
# do something..
end
Rails.logger.info("End tableau_contains check..")
end
Zweite Zeile in den after_save
Verfahren eifrig Lasten benötigen die Daten, die die tableau_contains
Prüfung durchzuführen. Die Anrufe wie tableau.tableau_cards
und tc.deck_card.card
sollte / wird nicht getroffen, die DB.
Issues in Ihrem Code:
1) Zuweisen von Array zu einer has_many
Assoziation
@game.players = Player.find :all, :include => ...
Erklärung oben ist nicht eine einfache Zuordnungsanweisung. Es ändert die palyers
Tabellenzeilen mit dem game_id
des gegebenen Spiels.
Ich gehe davon aus, dass ist nicht das, was Sie wollen. Wenn Sie die DB-Tabelle überprüfen, werden Sie feststellen, dass die updated_time
der Spieler Tabelle
Reihen haben nach Zuordnung geändert.
Sie haben den Wert in eine separate Variable zuzuordnen, wie es in dem Codebeispiel in after_save
Verfahren gezeigt.
2) Hand codiert Assoziation SQL
Viele Stellen im Code Sie sind von Hand Codierung der SQL für Zuordnungsdaten. Rails bietet Verbände für diese.
Z. B:
tcards= TableauCard.find :all, :include => [ {:deck_card => (:card)}],
:conditions => ['tableau_cards.tableau_id = ?', self.tableau.id]
Kann neu geschrieben werden als:
tcards = tableau.tableau_cards.all(:include => [ {:deck_card => (:card)}])
Die tableau_cards
Karten Assoziation auf Tableau
Modell baut die gleiche SQL Sie Hand codiert haben.
Sie können weiter die Aussage verbessern oben durch einen has_many :through
Verein Player
Klasse hinzufügen.
class Player
has_one :tableau
has_many :tableau_cards, :through => :tableau
end
tcards = tableau_cards.all(:include => [ {:deck_card => (:card)}])
Bearbeiten 1
habe ich eine Anwendung diesen Code zu testen. Es funktioniert wie erwartet. Rails läuft mehrere SQL zu eifrig Last der Daten, das heißt:.
Begin eager loading..
SELECT * FROM `players` WHERE (`players`.game_id = 1)
SELECT `tableau`.* FROM `tableau` WHERE (`tableau`.player_id IN (1,2))
SELECT `tableau_cards`.* FROM `tableau_cards`
WHERE (`tableau_cards`.tableau_id IN (1,2))
SELECT * FROM `deck_cards` WHERE (`deck_cards`.`id` IN (6,7,8,1,2,3,4,5))
SELECT * FROM `cards` WHERE (`cards`.`id` IN (6,7,8,1,2,3,4,5))
End eager loading..
Begin tableau_contains check..
End tableau_contains check..
Ich sehe keine der Daten nach dem eifrigen Laden ausgeführt SQL.
Edit 2
Nehmen Sie die folgende Änderung an Ihrem Code.
def after_save(pa)
turn = Turn.find(pa.turn_id, :include => :player_actions)
game = Game.find(turn.game_id, :include => :goals)
players = game.players.all(:include => [ :player_goals, {:tableau => [:tableau_cards => [:deck_card => [:card]]]} ])
if turn.phase_complete(pa, game, players)
for player in game.players
if(player.tableau_contains(card))
...
end
end
end
end
def phase_complete(phase, game, 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
Das Caching funktioniert wie folgt:
game.players # cached in the game object
game.players.all # not cached in the game object
players = game.players.all(:include => [:player_goals])
players.first.player_goals # cached
Die zweite Anweisung obigen Ergebnisse in einer benutzerdefinierten Assoziation Abfrage. Daher AR Cache nicht die Ergebnisse. Wo, wie player_goals
sind für jeden Spieler-Objekt in der dritten Anweisung im Cache gespeichert, wie sie Standard-Verein mit SQL abgerufen werden.
Andere Tipps
Problem Nummer eins ist: Sie sind das Zurücksetzen des player.tableau.tableau_cards jedes Mal
player.tableau.tableau_cards = TableauCard.find :all, :include => [ {:deck_card => (:card)}], :conditions => ['tableau_cards.tableau_id = ?', player.tableau.id]
Wenn das soll ein temporäres Array sein, dann Sie tun, mehr Arbeit als nötig. In der folgenden wäre besser:
temp_tableau_cards = TableauCard.find :all, :include => [ {:deck_card => (:card)}], :conditions => ['tableau_cards.tableau_id = ?', player.tableau.id]
Ich würde auch die beiden Operationen trennen, wenn Sie tatsächlich versuchen, die tableau_cards zu setzen und etwas tun, um sie.
player.tableau.tableau_cards = TableauCard.find :all, :include => [ {:deck_card => (:card)}], :conditions => ['tableau_cards.tableau_id = ?', player.tableau.id]
card.whatever_logic if player.tableau.tableau_cards.include? card
Auch hier sieht es aus wie Sie auf der Abfrage verdoppeln, wenn Sie nicht tun müssen.
Was passiert, wenn Sie den cards = TableauCard.find...
Anruf aus dem player.tableau.tableau_cards = cards
Anruf trennen out? Vielleicht Schienen ist an diesem Punkt in dem Code des Vereins im Cache gespeicherten Aufzeichnungen zurückzusetzen, und die Verbände danach neu geladen wird.
Dies würde auch erlauben Sie sicher, dass das gleiche Array machen wird, indem die Variable in explizit in tableau_contains
weitergegeben werden.
Es scheint, dass Sie versuchen, eifrig belasteten Verbände über mehrere Anrufe an dem player.cards.tableau_cards
Verein zu halten. Ich bin mir nicht sicher, ob diese Funktionalität möglich ist, mit der Art und Weise Schienen funktioniert. Ich glaube, dass es die Rohdaten-Caches aus einer SQL-Anweisung zurückgegeben, aber nicht das eigentliche Array, der zurückgegeben wird. Also:
def test_association_identity
a = player.tableau.tableau_cards.all(
:include => {:deck_card => :card})
#=> Array with object_id 12345
# and all the eager loaded deck and card associations set up
b = player.tableau.tableau_cards
#=> Array 320984230 with no eager loaded associations set up.
#But no extra sql query since it should be cached.
assert_equal a.object_id, b.object_id #probably fails
a.each{|card| card.deck_card.card}
puts("shouldn't have fired any sql queries,
unless the b call reloaded the association magically.")
b.each{|card| card.deck_card.card; puts("should fire a query
for each deck_card and card")}
end
Die einzige andere Sache, die ich von zu Hilfe denken kann, ist eine Ausgabe über den gesamten Code zu streuen und sehen, wo genau der träges Laden geschieht.
Hier ist, was ich meine:
#Observer
def after_save(pa)
@game = Game.find(turn.game_id, :include => :goals)
@game.players = Player.find( :all,
:include => [ {:tableau => (:tableau_cards)},:player_goals ],
:conditions => ['players.game_id =?', @game.id]
for player in @game.players
cards = TableauCard.find( :all,
:include =>{:deck_card => :card},
:conditions => ['tableau_cards.tableau_id = ?', player.tableau.id])
logger.error("First load")
player.tableau.tableau_cards = cards #See above comments as well.
# Both sides of this ^ line should always be == since:
# Given player.tableau => Tableau(n) then Tableau(n).tableau_cards
# will all have tableau_id == n. In other words, if there are
# `tableau_cards.`tableau_id = n in the db (as in the find call),
# then they'll already be found in the tableau.tableau_cards call.
logger.error("Any second loads?")
if(tableau_contains(cards,card))
logger.error("There certainly shouldn't be any loads here.")
#so that we're not relying on any additional association calls,
#this should at least remove one point of confusion.
...
end
end
end
#Also in the Observer, for just these purposes (it can be moved back out
#to Player after the subject problem here is understood better)
def tableau_contains(cards,card_id)
contains = false
logger.error("Is this for loop loading the cards?")
for card in cards
logger.error("Are they being loaded after `card` is set?")
# my logic here, looking at attributes of the Card model, with
# card.deck_card.card;
logger.error("What about prior to this call?")
end
return contains
end