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
War es hilfreich?

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
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top