1

積極的な読み込みでN+1クエリの問題を回避しようとしていますが、機能していません。関連するモデルはまだ個別にロードされています。

関連するActiveRecordsとそれらの関係は次のとおりです。

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

私が使用しているクエリは、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

それはスコープと関係がありますか?このtableau_containsメソッドは、より大きなループでいくつかのメソッド呼び出しを実行します。これらの同じオブジェクトがループされて調べられる場所がいくつかあるため、最初は積極的な読み込みを試みました。次に、ループの直前にロードを使用して、最終的に上記のコードを試しましたが、ログのtableau_cardsループ内のCardに対する個々のSELECTクエリがまだ表示されています。tableau_cardsループの直前にもIN句を使用した遅延読み込みクエリが表示されます。

編集:より大きな外側のループを含む以下の追加情報

EDIT2:回答からのヒントで以下のループを修正

EDIT3:目標を含むループに詳細を追加

これがより大きなループです。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

ターンクラスの関連コードは次のとおりです。

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プレーヤーをロードするために別のクエリを実行しています。キャッシュされます。つまり、ログにCACHEラベルがありますが、game.playersはすでにメモリにロードされているはずなので、クエリはまったくないと思います。

目標モデルからの別のスニペット:

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
4

3 に答える 3

6

これを試して:

class Game
  has_many :players
end

のロジックをtableau_contains次のように変更します。

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

のロジックをafter_save次のように変更します。

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

メソッドの2行目は、チェックafter_saveの実行に必要なデータを熱心にロードします。tableau_containsなどの呼び出しは、DBにヒットする必要がtableau.tableau_cardsありますtc.deck_card.card/ヒットしません。

コードの問題:

has_many1)アソシエーションへの配列の割り当て

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

上記のステートメントは、単純な割り当てステートメントではありません。指定されたゲームのpalyersテーブル行を変更します。game_id私はそれがあなたが望むものではないと思います。DBテーブルを確認すると、updated_time割り当て後にプレーヤーテーブルの行が変更されていることがわかります。

メソッドのコードサンプルに示されているように、値を別の変数に割り当てる必要がありますafter_save

2)手書きの関連付けSQL

コード内の多くの場所で、関連付けデータのSQLを手動でコーディングしています。Railsはこのための関連付けを提供します。

例えば:

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

次のように書き直すことができます。

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

モデルのtableau_cardsカードの関連付けによりTableau、手動でコーディングしたものと同じSQLが作成されます。

クラスにhas_many :through関連付けを追加することで、上記のステートメントをさらに改善できます。Player

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

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

編集1

このコードをテストするためのアプリケーションを作成しました。期待どおりに動作します。RailsはいくつかのSQLを実行して、データを熱心にロードします。

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

データを積極的に読み込んだ後、SQLが実行されていないのがわかります。

編集2

コードに次の変更を加えます。

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

キャッシングは次のように機能します。

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

上記の2番目のステートメントは、カスタムアソシエーションクエリになります。したがって、ARは結果をキャッシュしません。一方、標準の関連付けSQLを使用してフェッチplayer_goalsされるため、3番目のステートメントのすべてのプレーヤーオブジェクトに対してキャッシュされます。

于 2010-05-03T05:41:25.890 に答える
1

問題の1つは、player.tableau.tableau_cardsを毎回リセットしていることです。

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

それが一時的な配列であると想定される場合は、必要以上の作業を行っています。次の方が良いでしょう:

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

また、実際にtableau_cardsを設定して何かを実行しようとしている場合は、2つの操作を分離します。

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

繰り返しになりますが、必要のないときにクエリを2倍にしているようです。

于 2010-04-29T18:45:58.217 に答える
1

cards = TableauCard.find...通話と通話を分離するとどうなりplayer.tableau.tableau_cards = cardsますか?おそらく、railsは、コード内のその時点でアソシエーションのキャッシュされたレコードをリセットし、その後アソシエーションをリロードしています。

tableau_containsこれにより、変数を明示的に渡すことで、同じ配列が渡されていることを確認することもできます。

アソシエーションへの複数の呼び出しにわたって、熱心にロードされたアソシエーションを保持しようとしているようですplayer.cards.tableau_cards。この機能がレールの動作方法で可能かどうかはわかりません。SQLステートメントから返された生データはキャッシュされますが、返される実際の配列はキャッシュされないと思います。それで:

  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

私が助けると考えることができる他の唯一のことは、コード全体にいくつかの出力を分散させ、遅延読み込みが発生している場所を正確に確認することです。

これが私が意味することです:

#観察者

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
于 2010-04-30T20:01:31.747 に答える