Pregunta

Estoy tratando de evitar el problema N + 1 consultas con la carga ansiosa, pero que no está funcionando. Los modelos asociados todavía se están cargando individualmente.

Estos son los ActiveRecords relevantes y sus relaciones:

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

y la consulta que estoy usando es el interior de este método de jugador:

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

¿Tiene que ver con el alcance? Este método es tableau_contains abajo unas cuantas llamadas a métodos en un lazo más grande, donde originalmente intentado hacer la carga ansiosa porque hay varios lugares en los que estos mismos objetos se enlazan a través y examinaron. Entonces, finalmente, he intentado el código, ya que está por encima, con la carga justo antes del bucle, y todavía estoy viendo las consultas SELECT individuales para la tarjeta dentro del bucle tableau_cards en el registro. Puedo ver la consulta deseosos de carga con la cláusula IN justo antes del bucle tableau_cards también.

EDIT: información adicional a continuación con el bucle más grande, exterior

Edit2: bucle corregido a continuación con los consejos de respuestas

Edit3: añadido más detalles en bucle con los objetivos

Este es el lazo más grande. Está dentro de un observador en 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

Este es el código correspondiente en la clase a su vez:

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

for player in game.players está haciendo la otra consulta para cargar los jugadores. Se almacena en caché, que quiere decir que tiene la etiqueta de caché en el registro, pero yo habría pensado que no habría consulta en absoluto, porque los game.players ya deberían ser cargados en la memoria.

Otro fragmento del modelo Objetivo:

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
¿Fue útil?

Solución

Prueba esto:

class Game
  has_many :players
end

Cambiar la lógica de tableau_contains de la siguiente manera:

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

Cambiar la lógica de after_save de la siguiente manera:

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

Segunda línea en el método after_save cargas ansiosos los datos necesarios para realizar la comprobación de tableau_contains. Las llamadas como tableau.tableau_cards y tc.deck_card.card deben / no llegará a la base de datos.

Problemas en el código:

1) Asignación de matriz a una asociación has_many

@game.players = Player.find :all, :include => ...

Declaración anterior no es una sentencia de asignación simple. Cambia las filas de la tabla palyers con el game_id del juego dado. Estoy asumiendo que no es lo que desea. Si se comprueba la tabla de base de datos se puede observar que la updated_time de la mesa de los jugadores filas han cambiado después de la asignación.

Tiene que asignar el valor a una variable independiente, como se muestra en el ejemplo de código en el método after_save.

2) Mano asociación codificado SQL

Muchos lugares en el código que está codificando el SQL de datos de asociación de la mano. Rieles proporciona asociaciones para esto.

por ejemplo:

tcards= TableauCard.find :all, :include => [ {:deck_card => (:card)}], 
         :conditions => ['tableau_cards.tableau_id = ?', self.tableau.id]

se puede reescribir como:

tcards = tableau.tableau_cards.all(:include => [ {:deck_card => (:card)}])

La asociación tarjetas tableau_cards en el modelo Tableau construye el mismo SQL ha codificado mano.

Se puede mejorar aún más la afirmación anterior mediante la adición de una asociación has_many :through a clase Player.

class Player
  has_one :tableau
  has_many :tableau_cards, :through => :tableau
end

tcards = tableau_cards.all(:include => [ {:deck_card => (:card)}])

Editar 1

He creado una aplicación para probar el código. Funciona como se esperaba. Raíles funciona varios SQL para la carga ansiosa los datos, es decir:.

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..

No veo ninguna SQL ejecutada después de la carga ansiosa los datos.

Editar 2

Hacer el siguiente cambio en el código.

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

El almacenamiento en caché funciona de la siguiente manera:

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

La segunda declaración encima de los resultados de una consulta asociación personalizado. Por lo tanto AR no almacena en caché los resultados. Donde como player_goals se almacenan en caché para cada objeto jugador de la tercera afirmación, ya que se recuperan utilizando SQL asociación estándar.

Otros consejos

El problema número uno es: Usted está reposicionando la player.tableau.tableau_cards cada

player.tableau.tableau_cards = TableauCard.find :all, :include => [ {:deck_card => (:card)}], :conditions => ['tableau_cards.tableau_id = ?', player.tableau.id] 

Si eso se supone que es una matriz temporal, entonces están haciendo más trabajo de lo necesario. El siguiente sería mejor:

temp_tableau_cards = TableauCard.find :all, :include => [ {:deck_card => (:card)}], :conditions => ['tableau_cards.tableau_id = ?', player.tableau.id] 

También se separaría las dos operaciones si en realidad se está tratando de establecer las tableau_cards y hacer algo con ellos.

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

Una vez más, parece que se está duplicando arriba en la consulta cuando no es necesario.

¿Qué pasa si usted separadas a cabo la llamada cards = TableauCard.find... de la llamada player.tableau.tableau_cards = cards? Tal vez los carriles está restableciendo los registros almacenados en caché de la asociación en ese punto en el código, y volver a cargar las asociaciones después de eso.

Esto también permitiría que usted pueda asegurarse se está pasando la misma matriz en tableau_contains pasando la variable en forma explícita.

Parece que usted está tratando de mantener asociaciones-ansiosos cargado a través de múltiples llamadas a la asociación player.cards.tableau_cards. No estoy seguro de si esta funcionalidad es posible con la forma en que los carriles obras. Creo que almacena en caché los datos en bruto de regresar de una instrucción SQL, pero no la matriz real que se devuelve. Por lo tanto:

  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

La única otra cosa que puedo pensar para ayuda es dispersar parte de la producción en todo el código y ver exactamente donde la carga diferida está sucediendo.

Esto es lo que quiero decir:

#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
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top