为什么这个rails关联在急切加载后单独加载?

use*_*270 1 activerecord ruby-on-rails associations eager-loading

我试图通过急切加载避免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
Run Code Online (Sandbox Code Playgroud)

我正在使用的查询是在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
Run Code Online (Sandbox Code Playgroud)

它与范围有关吗?这个tableau_contains方法是在一个更大的循环中进行一些方法调用,我最初尝试进行预先加载,因为有几个地方将这些相同的对象循环并进行检查.然后我最终尝试了上面的代码,在循环之前加载了,我仍然在日志中的tableau_cards循环中看到了针对Card的各个SELECT查询.我也可以在tableau_cards循环之前看到带有IN子句的eager-loading查询.

编辑:下面的更多信息与更大的外循环

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
Run Code Online (Sandbox Code Playgroud)

这是转弯类中的相关代码:

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
Run Code Online (Sandbox Code Playgroud)

for player in game.players做另一个查询加载的球员.它是缓存的,我的意思是它在日志中有CACHE标签,但我认为根本就没有查询,因为game.players应该已经加载到内存中了.

Goal模型的另一个片段:

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
Run Code Online (Sandbox Code Playgroud)

Har*_*tty 6

试试这个:

class Game
  has_many :players
end
Run Code Online (Sandbox Code Playgroud)

改变逻辑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
Run Code Online (Sandbox Code Playgroud)

改变逻辑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
Run Code Online (Sandbox Code Playgroud)

方法中的第二行after_save急切加载执行tableau_contains检查所需的数据.调用诸如tableau.tableau_cardstc.deck_card.card/应该/不会命中数据库.

代码中的问题:

1)将数组分配给has_many关联

@game.players = Player.find :all, :include => ...
Run Code Online (Sandbox Code Playgroud)

上述声明不是简单的赋值声明.它palyers使用game_id给定游戏更改表格行.我假设这不是你想要的.如果检查数据库表,您会注意到updated_time分配后播放器表行的更改.

您必须将值分配给单独的变量,如方法中的代码示例所示after_save.

2)手动编码关联SQL

代码中的许多地方都是为关联数据编写SQL的代码.Rails为此提供关联.

例如:

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

可以改写为:

tcards = tableau.tableau_cards.all(:include => [ {:deck_card => (:card)}])
Run Code Online (Sandbox Code Playgroud)

模型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)}])
Run Code Online (Sandbox Code Playgroud)

编辑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..
Run Code Online (Sandbox Code Playgroud)

在急切加载数据后,我没有看到任何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
Run Code Online (Sandbox Code Playgroud)

缓存的工作原理如下:

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
Run Code Online (Sandbox Code Playgroud)

上面的第二个语句导致自定义关联查询.因此AR不会缓存结果.在player_goals使用标准关联SQL获取第3个语句中的每个玩家对象时缓存的位置.